ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Chapter7 C언어로 컴파일하기
    공부/High Performance Python 2024. 8. 21. 12:01
    728x90

    코드를 빠르게 하는 가장 쉬운 방법?

    처리할 작업의 양을 줄이는 것

    최적의 알고리즘을 사용해라~

    그 다음으로는 수행할 명령의 수를 줄이는 방법

    명령의 수를 줄이려면 코드를 기계어로 컴파일

     

    사이썬 - 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):

     

    댓글

Designed by Tistory.