[Cython] [Python] 고성능 Python을 위한 Cython 활용하기 1편

안녕하세요. Teus입니다.

 

지난 포스팅에 다양한 Python의 종류에 대해서 다뤘습니다.

그중에 Cython이라고 해서

CPython 패키지를 만들 수 있는 라이브러리가 있다고 하였습니다.

image.png

이번 포스팅은 고성능 Python포스팅 연재로,

Cython을 이용하는 방법에 대한 포스팅 1편 입니다.

 

1. Cython with python

Python이 CPython이라는 구현체로 되어있다고 지난 포스팅에서 소개했습니다.

 

때문에 CPython Level로 작성된 패키지는 굉장히 빠르게 동작합니다

(Ex. Python의 range, array 등등...)

 

하지만 CPython으로 패키지를 만드는건

  1. CPython에 대한 높은 숙련을 요구하고(메모리 관리 및 Reference Count관리를 패키지 작성자가 책임져야됨)

  2. Python 버전에 따라 호환성이 바뀌게 됩니다.(내부 CPython 구현체의 코드의 변경)

Cython은 Cython문법(Pyrex를 기초로함)으로 작성된 코드를 Python 버전에 맞게 CPython으로 변환해주는 역할을 해줍니다.


Cython은 C언어와 Python의 형식 중간에 있는 문법을 가지고 있습니다.

아래 공식문서에 나온 예시를 보도록 하겠습니다.

#integrate.py
def f(x):
    return x ** 2 - x


def integrate_f(a, b, N):
    s = 0
    dx = (b - a) / N
    for i in range(N):
        s += f(a + i * dx)
    return s * dx
#integrate_cython.pyx
def f(double x):
    return x ** 2 - x


def integrate_f(double a, double b, int N):
    cdef int i
    cdef double s
    cdef double dx
    s = 0
    dx = (b - a) / N
    for i in range(N):
        s += f(a + i * dx)
    return s * dx
#cython
integrate_f(10, 100, 10000000) ==> 0.70242
#Python
integrate_f(10, 100, 10000000) ==> 1.97632
  1. python파일과 달리, cython으로 작성 된 코드는 filenm.pyx 라는 확장자로 저장합니다.

  2. Python 코드와 달리 입력 매개변수와 함수 내에서 사용할 변수의 Type을 정해서 미리 선언해 줘야합니다.(선언하지 않더라도 가능은 하지만, 기존 Python의 속도로 돌아가게 됩니다)

적분을 Cython으로 구현한 코드와 Python으로 구현한 코드를 비교하면 두배이상 빠른것을 볼 수 있습니다

 

무슨일이 벌어진 것일까요?

 

일단 아래와 같은 setup.py를 설정후 command창에서 setup.py를 실행시켜 봅니다.

#setup.py
from setuptools import setup
from Cython.Build import cythonize

setup(
    name='intg_cython',
    ext_modules=cythonize("integrate_cython.pyx"),
    zip_safe=False,
)
$ python setup.py build_ext --inplace

setup.py 실행 결과 integrate_cython.c라는 C언어 파일이 생성되는것을 볼 수 있습니다.

 

해당 파일 내부에서 def f의 CPython의 구현을 확인해 볼 수가 있습니다.

// #define __Pyx_XDECREF(r) Py_XDECREF(r)
// #define __Pyx_GOTREF(r)  __Pyx_RefNanny->GOTREF(__pyx_refnanny, (PyObject *)(r), __LINE__)
// #define __Pyx_MODULE_NAME "integrate_cython"

static PyObject *__pyx_pf_16integrate_cython_f(CYTHON_UNUSED PyObject *__pyx_self, double __pyx_v_x) {
  PyObject *__pyx_r = NULL;
  __Pyx_RefNannyDeclarations
  PyObject *__pyx_t_1 = NULL;
  int __pyx_lineno = 0;
  const char *__pyx_filename = NULL;
  int __pyx_clineno = 0;
  //해당 fucntion의 이름을 context에 저장
  __Pyx_RefNannySetupContext("f", 0);

  /* "integrate_cython.pyx":2
 * def f(double x):
 *     return x ** 2 - x             # <<<<<<<<<<<<<<
 * 
 * 
 */
  __Pyx_XDECREF(__pyx_r);
  //return으로 반환할 값을 PyObject형식으로 변환 및 예외처리
  __pyx_t_1 = PyFloat_FromDouble((pow(__pyx_v_x, 2.0) - __pyx_v_x)); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 2, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_1);
  //ret값을 __pyx_t_1에 저장된 값을 가지고와서 할당함
  __pyx_r = __pyx_t_1;
  __pyx_t_1 = 0;
  goto __pyx_L0;

  /* "integrate_cython.pyx":1
 * def f(double x):             # <<<<<<<<<<<<<<
 *     return x ** 2 - x
 * 
 */

  /* function exit code */
  __pyx_L1_error:;
  __Pyx_XDECREF(__pyx_t_1);
  __Pyx_AddTraceback("integrate_cython.f", __pyx_clineno, __pyx_lineno, __pyx_filename);
  __pyx_r = NULL;
  __pyx_L0:;
  __Pyx_XGIVEREF(__pyx_r);
  __Pyx_RefNannyFinishContext();
  return __pyx_r;
}

...
static struct PyModuleDef __pyx_moduledef = {
    PyModuleDef_HEAD_INIT,
    "integrate_cython",
    0, /* m_doc */
  #if CYTHON_PEP489_MULTI_PHASE_INIT
    0, /* m_size */
  #else
    -1, /* m_size */
  #endif
    __pyx_methods /* m_methods */,
  #if CYTHON_PEP489_MULTI_PHASE_INIT
    __pyx_moduledef_slots, /* m_slots */
  #else
    NULL, /* m_reload */
  #endif
    NULL, /* m_traverse */
    NULL, /* m_clear */
    NULL /* m_free */
};
...
  __pyx_m = PyModule_Create(&__pyx_moduledef);
  ...
  return __pyx_m
 

위 코드를통해서

  1. pyx에서 정의한 변수의 경우 __pyx_v_variable-name 형태로 저장된다.

  2. CPython의 Py_xxxREF 함수를 별도로 warping해서 사용한다

  3. 중간중간 REF를 추가 및 삭제하면서 variable의 Refer_count를 조정한다.

  4. pyx_t는 temp로 추정되며, 중간값을 저장하는 변수로 활용된다.

  5. 코드 후반부에 PyModuleDef Object를 만들고, 이 Obectj의 주소값을 받아서 PyModule_Create를 실행함

대충 위와같이 정리할 수 있으며, 결국 만들어진 C파일은 CPython으로 작성된 것을 알 수가 있습니다.

 

그렇기 때문에 아래와 같은 변환을 통해서 Cython코드가 동작하는것을 예상할 수 있습니다.

"""
cython code -> CPython code -> setup.py -> .pyd(PyDLL) code
"""

 

2. Cython을 쓰면 무엇을 얻는가

1. C/C++ Library를 직접 활용할 수 있다.

C의 standard library와 C++의 STL을 활용할 수 있습니다.

C++의 경우 별도의 컴파일 옵션은 설정할 경우 vector와 같은 STL을 활용할 수가 있게됩니다.

--> 덕분에 보다 효율적/고성능인 프로그래밍 설계가 가능해 집니다.

 

2. 메모리관리를 직접 해야한다.

Cython을 사용할 경우, C언어의 통곡의 벽 이었던 Pointer를 마주하게 됩니다.

많은 분들이 기억 나시겠지만,

C언어에서 상수가 아닌 숫자형 변수 크기의 array를 만들기 위해서는 해당 크기만큼 malloc을 통해서 메모리를 확보해야 합니다.

#https://cython.readthedocs.io/en/latest/src/tutorial/memory_allocation.html
import random
from libc.stdlib cimport malloc, free

def random_noise(int number=1):
    cdef int i
    # C 라이브러리인 malloc을 통해서 메모리를 확보
    cdef double *my_array = <double *> malloc(number * sizeof(double))    
    try:
        ran = random.normalvariate
        for i in range(number):
            my_array[i] = ran(0, 1)
        #포인터를 리턴할 경우 python에서 해당 값을 이해할 수 없음
        #때문에 Python이 인식 가능한 object로 만들어서 반환해줌
        return [x for x in my_array[:number]]
    finally:
        # return the previously allocated memory to the system
        free(my_array)
        
#함수 포인터를 사용하는 패턴
cdef int(*ptr_add)(int, int)
cdef int add(int a, int b):
    return a + b

ptr_add = add
print(ptr_add(1, 3))

 

3. openmp를 활용한 병렬처리.

Cython내에서는 range대신 prange라는 method를 제공하며

prange를 통해서 gil을 해제한 상태로 Embarrassingly parallel을 손쉽게 구현이 가능합니다.

경축! 아무것도 안하여 에스천사게임즈가 새로운 모습으로 재오픈 하였습니다.
어린이용이며, 설치가 필요없는 브라우저 게임입니다.
https://s1004games.com

 

아래는 Cython Tutorial에서 확인할 수 있는 간단한 Cython 병렬처리 예제입니다.

#https://cython.readthedocs.io/en/latest/src/tutorial/parallelization.html
from cython.parallel cimport prange
cimport cython
from libc.math cimport sin

import numpy as np

#반복문 중 반복문의 boundcheck여부
@cython.boundscheck(False)
@cython.wraparound(False)
def do_sine(double[:,:] input):
    cdef double[:,:] output = np.empty_like(input)
    cdef Py_ssize_t i, j
    
    #prange 메쏘드를 사용해서 row마다 thread가 별도로 처리
    for i in prange(input.shape[0], nogil=True):
        for j in range(input.shape[1]):
            output[i, j] = sin(input[i, j])
    return np.asarray(output)
//cython 컴파일 한 뒤 C언어 파일에서 아래와같은 #progma mp처리가됨
#pragma omp parallel
{
    #pragma omp for firstprivate(i) lastprivate(i) lastprivate(j)
    for (__pyx_t_8 = 0; __pyx_t_8 < __pyx_t_9; __pyx_t_8++){
        i = __pyx_t_8;
        /* body goes here */
    }
}

 

3. Cython을 사용할때 진입장벽

위와같은 장점이 있지만, Cython을 활용하는데 있어서 역시 통곡의 벽이 존재합니다.

 

1. 일반적인 Python에서 활용하던 list를 활용하기 어렵다.

Cython에서 Python List는 바로 활용할 수가 없습니다.

때문에 type을 명시할 때 object type으로 명시하게 되며, 해당 Type을 활용할 때는 Python인터프리터가 관여하게 됩니다.

때문에 Cython을 활용하더라도 생각보다 빠른 성능을 얻지 못하는 결과를 보게 됩니다.

 

아래는 Cython와 Python으로 reduce sum을 구현했을 때의 성능차이 입니다.

%%cython --annotate
def cython_reduce_sum(object input_list):
    cdef long long ret
    ret = 0
    for val in input_list:
        ret += val
    return ret

def python_reduce_sum(input_list):
    ret = 0
    for val in input_list:
        ret += val
    return ret

cython_reduce_sum([1 for _ in range(100000000)])
#==> 5.453547
python_reduce_sum([1 for _ in range(100000000)])
#==> 5.974197

이때 IPython의 magic keyword를 통해서 어느 부분에서 Python 인터프리터가 관여하게 되는지를 보다 살펴볼 수가 있습니다.

reduce sum을 구현하는 과정에서 반복문에서 주기적으로 Python 인터프리터가 관여되는것을 확인할 수 있습니다.

 

image.png

 

그리고 List를 접근하는 부분의 detail한 CPython코드를 통해서 CPython의 List API를 통해서 List Data에 접근하게 되는것을 확인할 수가 있습니다.

image.png

 

이때 Cython에서는 공식적으로 Python List가 아닌 Python의 array를 활용할 것을 추천하고 있습니다.

#https://cython.readthedocs.io/en/latest/src/tutorial/array.html
from cpython cimport array
import array
cdef array.array a = array.array('i', [1, 2, 3])
cdef int[:] ca = a

cdef int overhead(object a):
    cdef int[:] ca = a
    return ca[0]

#array.array를 직접 받지않고 해당 array의 Pointer를 받음으로써
#추가적인 오버헤드 없이 동작이 가능함
cdef int no_overhead(int[:] ca):
    return ca[0]

print(overhead(a))  # new memory view will be constructed, overhead
print(no_overhead(ca))  # ca is already a memory view, so no overhead

아래는 reduce sum을 array를 활용해서 구현한 경우 입니다

from cpython cimport array
import array
def cython_reduce_sum2(int[:] input_array, array_len):
    cdef int[:] parr = input_array
    cdef int ret = 0
    cdef int cnt = 0
    while cnt < array_len:
        ret += parr[cnt]
        cnt += 1
    return ret

import array
import time
dt = array.array("i", [1 for _ in range(100000000)])
st = time.perf_counter()
cython_reduce_sum2(dt, 100000000)
print(time.perf_counter() - st)
#==>1.529048

일반 Python 실행대비 빠른 속도를 확인할 수가 있습니다. 하지만 여기도 함정이 있습니다.

 

List를 만들고, 이 list를 array.array로 만드는데 생각보다 많은 시간이 소요됩니다.

(이는 예전 Multiprocessing을 다룰때 역시 문제점이 되었었죠)

import time
import array
array.array("i", [1 for _ in range(100000000)])
#==> 5.300749500000165
[1 for _ in range(100000000)]
#==> 3.5525482000000466

때문에 cython으로 빨라진 속도를 array로 만드는 속도가 잡아먹고, 큰 이점을 얻어가기 어렵습니다.

 

2. C언어의 문제를 그대로 상속받는다.

1..malloc 문제 : 효율적으로 프로그래밍이 가능하지만

활용할 때 malloc과정에서 실수를 할 경우 Python kernel이 죽고

반환을 때 free를 하지않을 경우 Python 인터프리터의 메모리 누수가 발생합니다.

 

2.. overflow문제

python에서는 별도 로직을 통해서 int값의 overflow가 발생하지 않고 2^64 이상의 숫자를 표현할 수가 있습니다.

하지만 Cython에서는 int, long, unsigned int, long, longlong 등의 type이 사용되기 때문에 예상치 못한 문제에 직면하게 될 수 있습니다.

def cython_reduce_sum(object input_list):
    cdef int ret = 0
    cdef int val
    for val in input_list:
        ret += val
    return ret

import time
print(cython_reduce_sum([i for i in range(1000000)]))
#==>704982704
print(sum([i for i in range(1000000)]))
#==>4999950000
'''
overflow가 일어난 만큼을 제외한 값이 계산된 것을 볼 수 있다.
4294967296(overflow) + 704982704 = 4999950000
'''

이렇게 사용하면서 다수의 지뢰가 존재합니다.

이때 이런 지뢰를 피하게 해주는 구원자가 있으니...

그렇습니다.

 

image.png

 

고성능 이야기할때 마다 눈치없이 빠지지도 않고 등장하는 또파이 이지만

Python의 구원자같은 Numpy를 쓸 경우 위와같은 문제점들을 해결할 수가 있습니다.

다음 포스팅에서는 Numpy를 활용해서 보다 효율적으로 Cython을 활용하는 법에 대해서 다룹니다.

 

[출처] https://devocean.sk.com/blog/techBoardDetail.do?ID=164580

 

 

 

 

 

 

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
58 [python][flask] (flask) windows에서 flask와 apache 연동 file 졸리운_곰 2023.01.17 354
57 [Python][flask] Flask 로 Rest API 구현하기 - 개발환경구축 file 졸리운_곰 2022.12.24 848
56 [python][flask] bitnami의 django 서버로 flask 서비스 file 졸리운_곰 2021.12.10 592
55 [웹서버] Flask + REST API + Swagger file 졸리운_곰 2021.04.04 431
54 Python Flask 로 간단한 REST API 작성하기 file 졸리운_곰 2021.04.04 769
53 [파이썬][Flask] [번역] Flask 에서 백그라운드 작업을 처리하는 방법 file 졸리운_곰 2021.03.04 901
52 FLASK를 이용하여 PYTHON에서 PYTORCH를 REST API로 배포하기 졸리운_곰 2021.02.09 454
51 [flask 트랜드 홈페이지 개발] 10 - 다음 뉴스 키워드로 크롤링 file 졸리운_곰 2020.12.03 399
50 [python 트랜드 홈페이지개발] 9 - Flask POST file 졸리운_곰 2020.12.03 344
49 [python 트랜드 홈페이지개발] 8 - 약간의 레이아웃 설정하기 file 졸리운_곰 2020.12.03 518
48 [python 트랜드 홈페이지개발] 7 - 개요 file 졸리운_곰 2020.12.03 550
47 [python 트랜드 홈페이지개발] 6 - href 연결하기 file 졸리운_곰 2020.12.03 604
46 [python 트린드 홈페이지개발] 5 - 다른 페이지 크롤링 file 졸리운_곰 2020.12.01 506
45 [python 트린드 홈페이지개발] 4 - flask에 css 적용하기 file 졸리운_곰 2020.12.01 569
44 [python 트린드 홈페이지개발] 3 - 크롤링한 데이터 html에 보여주기 file 졸리운_곰 2020.12.01 546
43 [python 트린드 홈페이지개발] 2 - flask 프로젝트 생성, 세팅 file 졸리운_곰 2020.12.01 460
42 [python 트린드 홈페이지개발] 1 - 트렌드 홈페이지 개발 개요 file 졸리운_곰 2020.12.01 450
41 Flask 에서 CKEditor 파일첨부 연동 졸리운_곰 2020.11.26 631
40 플라스크 (Flask) - 회원가입 기능 만들기 (MVC 패턴 ) file 졸리운_곰 2020.11.26 518
39 Simple Python Flask Program with MongoDB file 졸리운_곰 2020.11.07 441
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED