본문 바로가기

Computer/Python

파이썬 코드 C언어로 컴파일하기 (3) - 사이썬과 넘파이

반응형

파이썬 코드 C언어로 컴파일하기 (1)

파이썬 코드 C언어로 컴파일하기 (2)


이 포스트에서 살펴봤듯이 파이선의 리스트 객체는 실제 데이터가 아니라 데이터가 저장된 주소를 담고 있기에 리스트 객체가 가리키는 객체는 메모리의 어디든 존재할 수 있습니다. 따라서 역참조에 따른 부가비용이 발생합니다. 하지만 배열 객체는 기본 타입을 연속적인 RAM 블록에 저장하므로 주소 계산이 빠르고 파이썬에서는 array 모듈로 기본 타입에 대한 1차원 저장소를 제공합니다. 넘파이의 array 모듈을 사용하면 다차원 배열을 지원하죠.

따라서 파이썬에서 다음 주소를 요청하지 않고 연속적인 메모리 공간에 놓인 배열 객체를 이용해 오프셋을 이용해 다음 메모리 주소를 직접 계산하도록 접근한다면 파이썬 가상 머신을 호출할 필요가 없으므로 더 빨라지기를 기대할 수 있습니다. 그렇다면 numpy 배열 객체를 사이썬과 연동하려면 어떤 구현이 필요할까요?

리스트 대신 배열 객체를 사용하기 위해 memoryview를 통한 버퍼 인터페이스 구문을 활용합니다. 즉, zs를 "double complex[:] zs"로 선언함으로써 (버퍼 프로토콜, []) 1차원 데이터 블록이 있는 복소수 객체임을 표현할 수 있는 것이죠. 이렇게 버퍼 인터페이스를 활용함으로써 파이썬 객체를 다른 형태로 변환하지 않고도 메모리를 저수준으로 다른 C 라이브러리와 쉽게 공유할 수 있습니다.

# cpythonfn.pyx
import numpy as np
cimport numpy as np

def calculate_z(int maxiter, double complex[:] zs, double complex[:] cs):
    cdef unsigned int i, n
    cdef double complex z, c
    #output = [0] * len(zs)
    cdef int[:] output = np.empty(len(zs), dtype=np.int32)
    for i in range(len(zs)):
        n = 0
        z = zs[i]
        c = cs[i]
        while (z.real*z.real + z.imag*z.imag) < 4 and n < maxiter:
            z = z*z + c
            n += 1
        output[i] = n
    return output

위에서 $zs, cs$ 부분을 버퍼 인터페이스로 선언하여 사이썬이 numpy 배열 접근이 가능해졌습니다. 따라서 calculate_z 함수를 호출하는 부분에서 다음과 같이 변경합니다.

    zs = []
    cs = []
    for ycoord in y:
        for xcoord in x:
            zs.append(complex(xcoord, ycoord))
            cs.append(complex(c_real, c_img))

    zs_np = np.array(zs, np.complex128)
    cs_np = np.array(cs, np.complex128)

마지막으로 numpy 배열을 사이썬으로 컴파일하기 위해 setup.py 파일을 다음과 같이 수정합니다.

from distutils.core import setup
from Cython.Build import cythonize
import numpy as np

setup(ext_modules=cythonize("cythonfn.pyx", compiler_directives={"language_level": "3"}),
      include_dirs=[np.get_include()])

코드를 수정하고 컴파일 후 실행하면 이제 0.1초 정도 소요되네요! 기존에는 파이썬 복소수 객체를 역참조할 때마다 부가비용이 들었지만 (z=zs[i] 부분) 이런 변수의 네이티브 버전을 만들면 C 속도로 작동하기에 속도가 더 빨라졌다고 볼 수 있습니다.

반응형