-
Chapter7 C언어로 컴파일하기공부/High Performance Python 2024. 8. 21. 12:01728x90
코드를 빠르게 하는 가장 쉬운 방법?
처리할 작업의 양을 줄이는 것
최적의 알고리즘을 사용해라~
그 다음으로는 수행할 명령의 수를 줄이는 방법
명령의 수를 줄이려면 코드를 기계어로 컴파일
사이썬 - C언어로 컴파일하는데 사용하는 가장 일반적 도구, numpy와 일반 파이썬 코드 모두 커버
Numba - numpy코드에 특화된 컴파일러
PyPy - 일반 파이썬 실행환경을 대체하는 비 numpy코드를 위한 JIT 컴파일러
7.1 가능한 속도 개선의 종류
컴파일로 빨라질 수 있는 부분은 대부분 수학적인 부분
why? 같은 연산을 무수히 반복하기 때문
이런 루프에서는 임시 객체를 많이 사용할 확률이 높음
근데 numpy연산은 임시 객체를 많이 생성하지 않아서 컴파일이 별 도움이 안 된다고 함
프로그램 동작 이해를 목표로 프로파일=>증거를 바탕으로 알고리즘 향상=>컴파일러나 JIT를 사용=>가성비 따지기
7.2 JIT 대 AOT 컴파일러
AOT : Ahead Of Time ex)사이썬
JIT : Just In Time ex)Numba, PyPy
AOT는 사용할 컴퓨터에 특화된 정적 라이브러리 생성
사용하기 전에 미리 컴파일하므로 코드에서 바로 해당 라이브러리 사용가능
JIT는 어떤 작업도 미리 하지 않고 컴파일러가 적절한 때에 컴파일 시작
그래서 초반에 느리게 실행됨
더보기근데 왜 AOT 사용 안 하는지?
AOT를 사용하려면 노오력이 많이 듦(AOT는 플랫폼에 종속적이라고 함, 플랫폼이 바뀔 때마다 노오력해줘야함)
7.3 타입 정보가 실행 속도에 영향을 주는 이유
파이썬은 동적 타입 언어
그래서 가상 머신에서 다음 연산에 사용할 데이터 타입을 알 수 없으므로 기계어 수준의 최적화가 힘듦
ex)abs함수
타입에 따라 다르게 동작함
정수나 실수는 단순 음수=>양수 변환
복소수는 실수와 허수의 제곱의 합의 제곱근을 반환=>얘가 더 오래 걸림
7.4 C 컴파일러 사용하기
GNU C 컴파일러 도구 모음의 gcc와 g++을 소개
gcc는 지원도 잘 되는 편에 성능도 좋은 편
플랫폼이나 CPU에 맞춰 튜닝한 컴파일러를 사용하기 때문에 성능은 좀 더 끌어올릴 수 있지만, 관련 지식이 필요
7.5 줄리아 집합 예제 다시 보기
줄리아 집합 ㄱ나니...?
얘는 정수와 복소수를 이용해서 결과 이미지를 생성했음=>CPU사용량이 많음코드에서 특히 CPU를 많이 사용하는 부분은 output리스트를 계산하는 내부 루프임
해당 리스트는 정사각형 모양의 픽셀 배열로 각 값은 해당 픽셀을 계산하는 데 든 비용을 나타냄
def calculate_z_serial_purepython(maxiter, zs, cs): output=[0]*len(zs) for i in range(len(zs)): n=0 z=zs[i] c=cs[i] while n<maxiter and abs(z)<2: z=z*z+c n+=1 output[i]=n return output
저자의 환경에선 크기가 1000*1000이고 maxiter=300인 줄리아 집합을 계산하는 과정이 대략 8초 정도 걸렸다고 함
7.6 사이썬
타입을 명시한 파이썬 코드를 컴파일된 확장 모듈로 변경해주는 컴파일러
타입 어노테이션은 C와 유사한 형태
확장 모듈은 import로 사용가능
OpenMP를 지원함
OpenMP 표준과 사이썬을 사용하면 한 컴퓨터의 여러 CPU에서 실행할 수 있도록 병렬 처리 문제를 다중 처리를 고려한 모듈로 변경가능=>근데 얘는 사이썬이 생성한 C코드 수준에서 동작한다고 함
7.6.1 사이썬을 사용하여 순수 파이썬 코드 컴파일하기
컴파일된 확장 모듈을 작성하는 과정에는 3가지 파일이 관여
- 호출하려는 파이썬 코드
- 새로 컴파일된 .pyx파일
- 확장 모듈을 작성하기 위해 사이썬을 호출하는 과정이 있는 setup.py파일
setup.py 스크립트에서 사이썬을 사용해서 .pyx파일을 컴파일
다음 파일들을 사용
- 입력 리스트를 생성하고 계산 함수를 호출하는 julia1.py
- CPU에 의존적인 함수가 있고 타입 어노테이션을 붙일 수 있는 cythonfn.pyx
- 빌드 과정을 담은 setup.py
setup.py를 실행하면 임포트할 수 있는 모듈이 생성됨
#julia1.py #... import cythonfn #as defined in setup.py setup.py에 정의 #... def calc_pure_python(desired_width, max_iterations): #... start_time=time.time() output=cythonfn.calculate_z(max_iterations, zs, cs) end_time=time.time() secs=end_time-start_time print(f"took {secs:0.2f} seconds") #... #cythonfn.pyx def calculate_z(maxiter, zs, cs): output=[0]*len(zs) for i in range(len(zs)): n=0 z=zs[i] c=cs[i] while n<maxiter and abs(z)<2: z=z*z+c n+=1 output[i]=n return output #setup.py from distutils.core import setup from Cython.Build import cyhonize setup(ext_modules=cythonize("cythonfn.pyx", compiler_directives={"language_level":"3"}))
$ python setup.py build_ext --inplace $ py julia1.py
7.7 pyximport
pyimport를 임포트하고 install을 호출하면 그 이후 임포트하는 .pyx파일은 자동으로 컴파일 됨
import pyximport pyximmport.install(language_level=3) import cythonfn
7.7.1 코드 블록을 분석하기 위한 사이썬 어노테이션
사이썬에는 결과를 HTML파일로 만들어내는 어노테이션 옵션이 있음(!
cython =a cythonfn.pyx를 실행하면 된다고 함
Cythonfn.html을 브라우저에서 확인한 결과 코드의 각 줄을 더블클릭하면 생성된 C코드 확인가능
짙을 수록 파이썬 가상 머신에서 더 많은 호출이 일어남, 옅을 수록 C코드가 더 많음을 의미
7.7.2 타입 어노테이션 추가하기
.pyx파일에 cdef 문법을 사용해 기본 타입 지정 가능
c언어처럼 함수 시작 부분에서 변수 선언해야함
def calculate_z(int maxiter, zs, cs): cdef unsigned int i,n cdef double complex z,c output=[0]*len(zs) for i in range(len(zs)): n=0 z=zs[i] c=cs[i] while n < maxiter and (z.real*z.real+z.imag*z.imag)<4: z=z*z+c n+=1 output[i]=n return output
abs()=>수식으로 변환하면?
원래는 (c.real**2+c.imag**2)**1/2 < 4**1/2를 계산해야했는데c.real**2+c.imag**2 < 4 로 변신!
=>더 빨라짐
같은 일을 하지만 좀 더 특화된 코드로 같은 문제를 해결하는 방법을 강도 저감(strength reduction)이라고 함실행을 빠르게 하려고 유연성과 가독성을 희생하는 방법
리스트 주소 찾을 때 경계 검사를 하지 않도록 하는 방법까지 추가
#cython : boundcheck=False
를 파이썬 첫머리에 주석 추가하면 됨
더보기경계 검사는 프로그램이 할당된 배열 밖의 데이터에 접근하지 않도록 막아줌
일반적으로는 리스트의 크기를 벗어나지 않도록 신경써야하는 배열 주소를 계속 계산해야하는 코드가 아니라면 사이썬에서 제공하는 경계검사를 해제해도 안전
7.8 사이썬과 넘파이
list객체가 가리키는 객체는 메모리의 어디든 존재할 수 있기 때문에 역참조에 따른 부가비용이 듦
배열 객체는 기본 타입을 연속적인 RAM 블록에 저장하므로 주소 계산이 빠름
array모듈은 기본타입에 1차원 저장소를 제공=>numpy.array를 사용하면 다차원 배열과 다양한 기본 타입을 저장할 수 있음
array객체를 미리 예측할 수 있는 패턴으로 이터레이션한다면, 파이썬에게 다음 주소를 요청해 받아오지 말고 직접 다음 메모리 주소를 계산해 접근하도록 컴파일러에게 지시할 수 있음
from cython.parallel import parallel, prange import numpy as np # numpy를 이용해서 객체의 타입을 고정하고 cimport numpy as np def calculate_z(int maxiter, double complex[:] zs, double complex[:] cs):# 타입을 넣고 자료구조도 변형해서 numpy로 array를 표현 """Calculate output list using Julia update rule""" cdef unsigned int i, length cdef double complex z, c cdef int[:] output = np.empty(len(zs), dtype=np.int32) length = len(zs) with nogil, parallel(): for i in prange(length, schedule="guided"): z = zs[i] c = cs[i] output[i] = 0 while output[i] < maxiter and (z.real * z.real + z.imag * z.imag) < 4:# abs()함수를 인라이닝해서 빠르게 만듦 z = z * z + c output[i] += 1 return output
7.8.1 한 컴퓨터에서 OpenMP를 사용해 병렬화하기
OpenMP란?
C, C++, 포트란에서 병렬 실행과 메모리공유를 지원하는 다중 플랫폼 API
적절히 작성된 C코드가 있다면 컴파일러 수준에서 병렬화해줌
사이썬에서는 prange연산자를 사용하고 setup.py에 -fopenmp 컴파일러 지시를 넣어서 OpenMP추가가능
# 사용법 # .pyx 파일에 추가 from cython.parallel import prange # setup.py에 컴파일러와 링커 플래그를 추가 ext_modules = [Extension( "cythonfn", ["cythonfn.pyx"], extra_compile_args=['-fopenmp'], extra_link_args=['-fopenmp'], )]
7.9 Numba
numpy코드에 특화된 JIT컴파일러
Numba는 새 환경에 설치하려면 오래 걸리므로, 모든 것이 포함된 컨티넘의 아나콘다 배포판을 까는 것을 추천한다고 함
from numba import jit @jit() def calculate_z_serial_purepython(maxiter,zs, cs, output):