본문으로 건너뛰기
Advertisement

C 확장과 대안

Cython, Numba, ctypes로 Python의 속도 한계를 극복합니다.


설치

pip install cython numba cffi

Numba — JIT 컴파일러 (가장 쉬운 방법)

from numba import njit, jit, prange
import numpy as np
import time


# ── 기본 @njit ────────────────────────────────────────────
@njit
def sum_squares(n: int) -> float:
total = 0.0
for i in range(n):
total += i * i
return total


# 첫 호출 시 컴파일 (워밍업)
sum_squares(100)

# 비교
start = time.perf_counter()
result_py = sum(i * i for i in range(10_000_000))
print(f"Python: {time.perf_counter() - start:.3f}s")

start = time.perf_counter()
result_nb = sum_squares(10_000_000)
print(f"Numba: {time.perf_counter() - start:.3f}s")


# ── NumPy 배열 처리 ───────────────────────────────────────
@njit(parallel=True) # 병렬 처리
def parallel_sum(arr: np.ndarray) -> float:
total = 0.0
for i in prange(len(arr)): # prange = 병렬 range
total += arr[i]
return total


arr = np.random.random(10_000_000)
result = parallel_sum(arr)


# ── 벡터화 ufunc ──────────────────────────────────────────
from numba import vectorize, float64


@vectorize([float64(float64, float64)])
def clip_and_scale(x, threshold):
if x > threshold:
return threshold
return x * 2.0


data = np.array([1.0, 3.0, 5.0, 2.0, 4.0])
result = clip_and_scale(data, 3.0) # NumPy ufunc처럼 작동

Cython — Python을 C로 컴파일

# fibonacci.pyx — Cython 소스 파일
# cdef: C 타입 선언으로 속도 향상

# --- fibonacci.pyx ---
# def fib_py(n): # 순수 Python 함수
# if n <= 1: return n
# return fib_py(n-1) + fib_py(n-2)
#
# cpdef long fib_c(long n): # C 타입 선언
# if n <= 1: return n
# return fib_c(n-1) + fib_c(n-2)
#
# cdef long fib_internal(long n): # C 전용 (Python에서 호출 불가)
# if n <= 1: return n
# return fib_internal(n-1) + fib_internal(n-2)

# --- setup.py ---
# from setuptools import setup
# from Cython.Build import cythonize
#
# setup(ext_modules=cythonize("fibonacci.pyx"))

# 빌드:
# python setup.py build_ext --inplace

# 사용:
# import fibonacci
# print(fibonacci.fib_c(40))
# optimized_math.pyx — 타입 어노테이션 완전 버전
import numpy as np
cimport numpy as cnp

def moving_average(cnp.ndarray[cnp.double_t, ndim=1] arr, int window):
"""이동 평균 — Cython 최적화"""
cdef int n = len(arr)
cdef cnp.ndarray[cnp.double_t, ndim=1] result = np.zeros(n)
cdef double total = 0.0
cdef int i

for i in range(n):
total += arr[i]
if i >= window:
total -= arr[i - window]
if i >= window - 1:
result[i] = total / window

return result

ctypes — C 라이브러리 직접 호출

import ctypes
import ctypes.util
import os
import sys


# ── 표준 C 라이브러리 함수 호출 ───────────────────────────
if sys.platform == "win32":
libc = ctypes.CDLL("msvcrt.dll")
else:
libc = ctypes.CDLL(ctypes.util.find_library("c"))

# printf
libc.printf(b"Hello from C: %d\n", 42)

# ── 커스텀 C 라이브러리 ────────────────────────────────────
# mathlib.c:
# double add(double a, double b) { return a + b; }
# int factorial(int n) { return n <= 1 ? 1 : n * factorial(n-1); }

# 빌드: gcc -shared -fPIC -o mathlib.so mathlib.c

# lib = ctypes.CDLL("./mathlib.so")
# lib.add.argtypes = [ctypes.c_double, ctypes.c_double]
# lib.add.restype = ctypes.c_double
# print(lib.add(3.14, 2.72))

# ── 구조체 정의 ───────────────────────────────────────────
class Point(ctypes.Structure):
_fields_ = [
("x", ctypes.c_double),
("y", ctypes.c_double),
]


class Rectangle(ctypes.Structure):
_fields_ = [
("top_left", Point),
("bottom_right", Point),
]


p = Point(1.0, 2.0)
print(f"Point({p.x}, {p.y})")

rect = Rectangle(Point(0, 0), Point(10, 5))
width = rect.bottom_right.x - rect.top_left.x
height = rect.bottom_right.y - rect.top_left.y
print(f"Rectangle {width} x {height}")

# ── 배열 ──────────────────────────────────────────────────
IntArray5 = ctypes.c_int * 5
arr = IntArray5(10, 20, 30, 40, 50)
for i in range(5):
print(arr[i])

cffi — 더 나은 C 인터페이스

from cffi import FFI

ffi = FFI()

# C 함수 시그니처 선언
ffi.cdef("""
double sqrt(double x);
int abs(int n);
""")

# 라이브러리 로드
if __import__("sys").platform == "win32":
lib = ffi.dlopen("msvcrt.dll")
else:
lib = ffi.dlopen(None) # 표준 라이브러리

result = lib.sqrt(16.0)
print(result) # 4.0

neg = lib.abs(-42)
print(neg) # 42

# 인라인 C 코드 컴파일
ffi_inline = FFI()
ffi_inline.cdef("int add(int a, int b);")

lib_inline = ffi_inline.verify("""
int add(int a, int b) {
return a + b;
}
""")

print(lib_inline.add(3, 4)) # 7

성능 비교

import timeit
import numpy as np

N = 1_000_000

# Pure Python
def py_sum_sq(n):
return sum(i * i for i in range(n))

# NumPy
def np_sum_sq(n):
arr = np.arange(n, dtype=np.float64)
return np.sum(arr * arr)

t_py = timeit.timeit(lambda: py_sum_sq(N), number=3)
t_np = timeit.timeit(lambda: np_sum_sq(N), number=3)

print(f"Python: {t_py:.3f}s")
print(f"NumPy: {t_np:.3f}s ({t_py/t_np:.0f}x faster)")
# Numba @njit 는 NumPy와 비슷하거나 더 빠름 (대규모 루프에서 특히)

정리

도구난이도속도 향상적합한 상황
Numba @njit쉬움10~100x수치 루프, NumPy 배열
Cython중간10~200x복잡한 로직, 타입 선언
ctypes어려움N/A기존 C 라이브러리 호출
cffi중간N/A더 안전한 C 인터페이스

일반적인 추천 순서: NumPy/Pandas 벡터화 → Numba @njit → Cython → ctypes/cffi

Advertisement