본문 바로가기
Study/Python

Cython으로 속도 향상 꾀하기

by 개발새-발 2022. 6. 30.
반응형

Preface

Python을 사용하면 적은 코드로 쉽게 기능을 구현할 수 있지만, C나 Cpp와 같은 언어에 비해 느립니다. 많은 작업을 최적화가 잘 된 라이브러리를 사용하여 처리할 수도 있겠지만, 원하는 작업을 수행하는 라이브러리가 없어 순수 python 코드로 작성하는 경우도 많습니다. 작성한 python 코드에 의한 속도 저하는 작은 데이터를 처리할 때는 신경쓰이지 않지만, 데이터가 커질 수록 실행속도가 느린 작업에 의해 우리가 기다려야 하는 시간이 눈에 띄게 됩니다.

가정

약 57만개의 row와 2개의 column으로 이루어진 names.csv 파일이 있습니다. 두 column은 어느 장소의 이름이고, 하고자 하는 작업은 두 column에 대해 LCS (Longest Common Sequence) 의 길이를 구하는 것입니다. LCS 의 길이를 구하기 위해 아래의 python 파일를 작성하여 사용하였습니다. 이 파일의 이름은 pylcs.py 입니다.

import numpy as np

def lcs(x,y) -> int:

    lx = len(x)
    ly = len(y)
    table = np.zeros((lx+1,ly+1),dtype=np.int32)

    for iy in range(0,ly):
        for ix in range(0, lx):
            if(x[ix] == y[iy]):
                table[ix+1,iy+1] = table[ix,iy] + 1
            else:
                table[ix+1,iy+1] = max(table[ix,iy+1],table[ix+1,iy])
    return table[lx,ly]

위 함수를 이용하여 names.csv의 모든 행에 위 lcs 함수를 실행하면 제 컴퓨터 기준으로 대략 1분 44초가 소요됩니다. ‘1분 44초 정도야 그저 기다리면 되지 않을까’ 싶지만, 다른 데이터에서 더 많은 row들과, 더 많은 column들을 처리를 하다보면 이 작업을 처리하는데 드는 시간을 줄이고 싶어집니다. 이제, 위 lcs 함수의 실행 속도를 Cython으로 향상시켜 봅시다.

Cython 설치

사용하기 전에 먼저 cython을 설치해줘야 합니다. 만약 Anaconda를 사용중이라면, 이미 bundle 되어있기 때문에 따로 설치할 필요는 없습니다.

Cython은 C 컴파일러를 필요로 합니다. 필요한 컴파일러는 운영체제마다 다른데, linux에서는 gcc, Windows에선 MS Visual C 혹은 MingGW 를 사용하는 방법이 존재합니다.

필요한 컴파일러의 설치를 완료한 후 아래 pip 명령어를 수행하면 설치가 됩니다.

pip install Cython

Cython 사용하기

.pyx 파일 작성

먼저 .pyx 파일을 작성합니다. 아래는 동일 폴더의 cylcs.pyx 파일입니다.

import numpy as np

cpdef int lcs(str x,str y):

    cdef int lx = len(x)
    cdef int ly = len(y)
    cdef int[:,:] table = np.zeros((lx+1,ly+1),dtype=np.int32)
    cdef int ix,iy

    for iy in range(0,ly):
        for ix in range(0, lx):
            if x[ix] == y[iy]:
                table[ix+1,iy+1] = table[ix,iy] + 1
            else:
                table[ix+1,iy+1] = max(table[ix,iy+1],table[ix+1,iy])
    return table[lx,ly]

python과 달리 각 변수의 type을 지정해 줄 수 있습니다.

위처럼 .pyx 파일을 작성하였다면, 수동으로 compile을 실행하여 import를 하거나 pyimport를 사용하여 import를 할 수 있습니다.

수동으로 compile 후 실행

수동으로 compile을 진행하기 위해, setup.py 를 먼저 작성해 주어야 합니다. cylcs.py 와 같은 위치에 작성해 줍시다.

from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize("cylcs.pyx", language_level = "3")
)

setup.py 를 작성하였다면, 해당 폴더에서 아래 명령어를 실행해 줍니다.

python setup.py build_ext --inplace

명령어가 수행되었다면, 폴더내에 .pyd (windows) 혹은 .so (linux) 파일이 생성되었을 것입니다. 이제 일반 python module을 import 하듯이 import를 해 주면 됩니다.

import cylcs

i = cylcs.lcs("abcdefg","anbdekg")

pyximport 로 사용하기

특별히 복잡한 빌드 옵션이 존재하지 않는다면, pyximport를 통해 import를 수행할 수 있습니다. cylcs.pyx 와 내용이 동일한 cylcs2.pyx 를 pyximport를 통해 import 해봅시다.

import pyximport
pyximport.install(language_level=3)

import cylcs2

j = cylcs2.lcs("abcdefg","anbdekg")

pyximport.install 에서 동일 폴더에 존재하는 .pyx 파일을 컴파일 하여 import 할 수 있도록 해줍니다. 수동으로 compile 하는 것보다 간편합니다. 그러나, 다른 c library를 필요로 하거나 setuptools 를 이용한 build option이 필요한 경우엔 수동으로 compile을 해 주어야 합니다.

Jupyter notebook에 작성하기

따로 파일을 만들어서 사용하지 않고, Jupyter Notebook에 바로 .pyx에 작성하였던 내용을 적을 수 있습니다. Jupyter에서 Cython을 사용하기 위해 먼저 extension을 load해 줍니다.

%load_ext cython

이후 어느 cell에 cython module을 작성하고자 할 때 그 cell의 가장 위에 %%cython 을 붙혀주면 됩니다. 위 lcs 함수를 어느 Jupyter cell 에 정의하고자 한다면 아래와 같이 됩니다.

%%cython

import numpy as np

cpdef int lcs(str x,str y):

    cdef int lx = len(x)
    cdef int ly = len(y)
    cdef int[:,:] table = np.zeros((lx+1,ly+1),dtype=np.int32)
    cdef int ix,iy

    for iy in range(0,ly):
        for ix in range(0, lx):
            if x[ix] == y[iy]:
                table[ix+1,iy+1] = table[ix,iy] + 1
            else:
                table[ix+1,iy+1] = max(table[ix,iy+1],table[ix+1,iy])
    return table[lx,ly]

이후 다른 함수를 사용하듯 사용하면 됩니다.

실행시간 비교

이제 Cython을 적용하여 성능이 향상되었는지 확인해 봅시다. names.csv의 578907개의 행에 대해 수행한 결과입니다.

  • only Python → 약 1분 44초
%%timeit

# only python
# 1min 44s ± 4.26 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
for x,y in zip(df_names['name_1'],df_names['name_2']):
    pylcs.lcs(x,y)
  • with Cython → 약 2.9초
%%timeit

# using cython
# 2.88 s ± 101 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
for x,y in zip(df_names['name_1'],df_names['name_2']):
    cylcs.lcs(x,y)

cython을 적용한 결과는 python으로 작성한 함수보다 35배 빨랐습니다.

실행시간에 대하여

위의 경우는 cython을 적용하여 실행시간 면에서 많은 이득을 보았습니다. 특히, python 자체 함수나 python object를 덜 사용할 수록 실행속도는 더 빨라졌습니다.

하지만, 모든 경우가 cython을 적용한다고 성능이 좋아지는 것은 아닙니다. 많은 양의 데이터에 대해 처리하는 것이 아니라면 pyximport와 직접 jupyter notebook에 작성하는 경우 컴파일을 하는 시간 때문에 총 실행 시간에 이득을 보지 못할 수 있습니다. 또, numpy의 vectorized 연산과 같이 이미 최적화가 되어있는 경우 직접 cython함수를 작성하는 것보다 numpy의 함수를 사용하는 것이 더 빠를 수 있습니다. 또, python 자체 함수나 object를 사용하는 경우에는 병목이 발생하기 때문에 원하는 만큼의 성능 향상을 보지 못할 수도 있습니다. Cython에서는 어느 부분에서 파이썬을 참조하는 지 알 수 있도록 보고서를 제공합니다. 아래는 jupyter notebook에 직접 코드를 작성하였을 때 %%cython -a 를 사용한 경우입니다.

Cython도 마술은 아닙니다.

다른 속도 향상 방법

직접 Cython을 위한 code를 작성하지 않고 속도 향상을 꾀하는 방법이 있습니다.

  • Numpy 와 같이 잘 최적화 된 라이브러리를 사용합니다.
  • numba library의 @njit annotation을 사용합니다.

numba 라이브러리의 njit annotation은 기존 파이썬 코드에 어노테이션을 붙이는 것 만으로도 cython으로 코드를 작성한 효과 혹은 그 이상을 낼 수 있도록 합니다. 추가적인 코드작성 없이 속도향상을 꾀할 수 있는 효과적인 방법중 하나입니다.

반응형

댓글