0. Cython?
line_profiler 같은 프로파일링 툴로 알고리즘을 최적화하여 CPU에서의 계산량, RAM에서의 병목을 어느 정도 개선했다면 노력 대비 속도 개선이 줄어드는, 즉 한계 효용이 감소하는 시기가 옵니다.
그러면 다음으로 시간을 줄이는 가장 쉬운 방법은 기계어로 컴파일하는 것입니다.
컴파일을 하게 되면 CPU에서 바로 실행 가능한 기계어로 변환되며,
이는 같은 연산을 반복하는 루프나 list, array에 대한 역참조, 수학 계산과 같은 곳에서 시간을 줄여줄 수 있습니다.
대표적으로 파이썬을 C언어로 컴파일 해주는 라이브러리인 사이썬(Cython)이 있습니다.
사이썬은 파이썬 코드를 컴파일된 확장 모듈로 변경해 주고 이 확장 모듈을 import 해서 쓰기만 하면 되기 때문에 비교적 사용이 쉬운 편입니다.
※ Cython과 Cpython의 차이
Cpython은 파이썬 구현체 중 하나입니다.
python을 c로 만들면 Cpython , java로 만들면 jython이라 부르고
우리가 평소 설치해서 사용하는 python은 c로 구현된 Cpython입니다.
1. Implements
사이썬을 사용하려면 다음과 같이 3가지 파일이 필요합니다.
- 컴파일 할 파이썬 코드
- .pxy 파일
- .pxy 파일을 컴파일할 setup.py
저는 컴파일 할 파이썬 코드로 전에 작성했던 '줄리아 집합' 생성 코드를 사용하겠습니다.
#julia_set.py
import numpy as np
import time
def julia_set(width, height, c, max_iter):
start_time = time.time()
x_min, x_max = -2.0, 2.0
y_min, y_max = -2.0, 2.0
x_range = np.linspace(x_min, x_max, width)
y_range = np.linspace(y_min, y_max, height)
img = np.empty((width, height))
for x in range(width):
for y in range(height):
zx, zy = x_range[x], y_range[y]
count = 0
while count < max_iter and abs(zx * zx + zy * zy) < 4.0:
z_next = zx * zx - zy * zy + c.real
zy, zx = 2.0 * zx * zy + c.imag, z_next
count += 1
img[x, y] = count
end_time = time.time()
print(f"Took {end_time-start_time:02f} seconds")
return img
if __name__ == "__main__":
width = 1000
height = 1000
c = complex(-0.7, 0.27015)
max_iter = 1024
julia_img = julia_set(width, height, c, max_iter)
$ python julia_set.py
Took 15.448039 seconds
이 코드는 다음과 같이 15.4초가 걸렸습니다.
이제 사이썬을 사용해 보겠습니다.
.pyx 파일을 만들어주고 julia_set.py 코드를 똑같이 복사해 줍니다.
#cython_julia_set.pyx
import numpy as np
import time
def julia_set(width, height, c, max_iter):
start_time = time.time()
x_min, x_max = -2.0, 2.0
y_min, y_max = -2.0, 2.0
x_range = np.linspace(x_min, x_max, width)
y_range = np.linspace(y_min, y_max, height)
img = np.empty((width, height))
for x in range(width):
for y in range(height):
zx, zy = x_range[x], y_range[y]
count = 0
while count < max_iter and abs(zx * zx + zy * zy) < 4.0:
z_next = zx * zx - zy * zy + c.real
zy, zx = 2.0 * zx * zy + c.imag, z_next
count += 1
img[x, y] = count
end_time = time.time()
print(f"Took {end_time-start_time:02f} seconds")
return img
그다음 setup.py를 다음과 같이 작성하고 실행시켜 줍니다.
여기서 사이썬을 사용해서 .pyd 파일을 컴파일을 하면 컴파일된 모듈이 생성됩니다.
유닉스 계열에서는 .so파일, 윈도우에서는 .pyd파일이 생성됩니다.
#setup.py
from distutils.core import setup
from Cython.Build import cythonize
setup(ext_modules=cythonize("cython_julia_set.pyx", compiler_directives={"language_level": "3"}))
$ python setup.py build_ext --inplace
Compiling cython_julia_set.pyx because it changed.
[1/1] Cythonizing cython_julia_set.pyx
running build_ext
building 'cython_julia_set' extension
...
$ python julia_set.py
Took 13.709749 seconds
이 코드는 다음과 같이 13.7초가 걸렸습니다.
실행시간이 줄어든 것 같은데 그렇게 획기적으로 줄어든 것 같지는 않습니다.
그 이유는 타입 어노테이션을 추가하지 않았기 때문입니다.
파이썬의 변수가 어떤 타입이라도 참조할 수 있고, 코드 어디서든 타입을 변경할 수 있는 동적 타입 언어입니다.
이 특징은 파이썬을 사용하기 쉽게 해 주지만 타입을 체크하는데 시간을 꽤 잡아먹음으로써 단점이 되기도 합니다.
따라서 타입을 지정하여 사이썬이 해석할 수 있도록 해줍니다.
그러면 사이썬은 이 타입을 이용하여 파이썬 코드를 C객체로 변환하여 파이썬 스택에서 호출되지 않도록 합니다.
이 부분에서 실행 속도가 빨라지는 것입니다.
추가한 타입은 다음과 같습니다.
- 실수 double
- 정수 int
import numpy as np
import time
def julia_set(int width, int height, double complex c, int max_iter):
cdef double start_time, end_time
cdef double x_min, x_max, y_min, y_max, zx, zy, z_next
cdef int x, y, count
start_time = time.time()
x_min, x_max = -2.0, 2.0
y_min, y_max = -2.0, 2.0
x_range = np.linspace(x_min, x_max, width)
y_range = np.linspace(y_min, y_max, height)
img = np.empty((width, height))
for x in range(width):
for y in range(height):
zx, zy = x_range[x], y_range[y]
count = 0
while count < max_iter and abs(zx * zx + zy * zy) < 4.0:
z_next = zx * zx - zy * zy + c.real
zy, zx = 2.0 * zx * zy + c.imag, z_next
count += 1
img[x, y] = count
end_time = time.time()
print(f"Took {end_time-start_time:02f} seconds")
return img
그리고 다시 setup.py파일을 실행해 컴파일 해줍니다.
$ python setup.py build_ext --inplace
$ python julia_set.py
Took 0.235214 seconds
실행시간이 0.23초로 처음 15.4초 대비 98배 빨라진 것을 확인할 수 있습니다.
2. Conclusion
이제 이 방법만 사용하면 무슨 코드든지 빨라질 수 있을 것 같지만
사실은 사용을 하지 못하는 경우가 많습니다.
정규식, DB, 입출력과 같은 외부 라이브러리를 호출하는 코드,
numpy와 같이 벡터 연산이 많고 라이브러리 자체가 C언어로 구현이 되어있는 경우에는 속도 개선을 기대하기 어렵습니다.
이 경우에는 C-API를 사용하여 직접 C언어로 코드를 작성해야 합니다.
C-API 또한 유용하니 이에 대해서는 다음에 다루도록 하겠습니다.
https://hanbit.co.kr/store/books/look.php?p_code=B8494674601
https://cython.readthedocs.io/en/latest/
'Python' 카테고리의 다른 글
[Python] 비동기 프로그래밍으로 크롤링 속도 개선(asyncio) (0) | 2023.11.12 |
---|---|
[Python] 프로파일링으로 병목 찾기 (line_profiler) (1) | 2023.10.09 |
[Python] 언어모델의 출력을 스트리밍 방식으로 출력하기 (1) | 2023.06.11 |
[Python] PEFT 라이브러리 알아보기 (0) | 2023.05.29 |
[Python] Folium으로 지도에 행정구역 경계 표시하기 (0) | 2023.02.26 |