레이블이 cython인 게시물을 표시합니다. 모든 게시물 표시
레이블이 cython인 게시물을 표시합니다. 모든 게시물 표시

20130831

python - cython

나는 Python이 좋다. 하지만 Python은 느리다
뭐가 느리냐 하면 지명적인 것이지만 사칙연산이 느리다. 하지만 동적으로 형이 정의되는 다른 스크립트 언어(Perl, Ruby, Javascript)도 C, Java처럼 컴파일을 하는 정적형 정의 언어에 비해 압도적으로 느리다.(최근에는 Javascript처럼 그물게 진보한 언어도 있으므로 필히 그렇다라고는 말할수 없다.)
스크립트 언어가 느린 원인중 하나는 변수의 형이 지정되어 있지 않으므로 형 체크를 매회 수행할 필요가 있기때문이다.이런 특징으로 인해 자동적으로 형을 변화해 오버로드를 방지해 주는 장점도 있지만 어찌하던간 정적형 지정언어보다가는 속도를 낼수 없다.
그렇다면 Python의 코드에 형지정을 더하여 컴파일을 해버리면 되지않는가라는 것이 Cython이다. 정확하게 말하면 Python라이크 문법으로 적을 코드를 C/C++로 변환하여 컴파일 한다. 거짓말을 보태어 단순한 계산의 경우 Python으로 코드를 실행하는것 보다 100배 이상도 속도를 낼수 있다고 한다. 자 그럼 실험으로 확인해 보자

Cython의 기본적인 사용방법은 공식 다큐멘트를 읽어 보도록 하자
일본어판
주의:이하의 벤치마크는 MacOS 10.7.3 MacBook Core2 Duo 2.26GHz에서 Python과 C/C++의 컴파일러로 Mac표준의 Python2.7.1, llvm-gcc를 사용하고 있다.
C/C++는 time커맨드, Python/Cython는 ipython의 timeit을 이용하여 실험시간을 측정한다.
그리고 이하의 기록은 환경과 구현에 따라 결과가 다를수 있다.

단순한 가산

먼저 단순한 가산을 비교해 보자. 1~100000000의 수를 더한다.
C
$ gcc -O2 simple_add.c
$ time ./a.out

real     0m0.004s
user     0m0.001s
sys     0m0.003s
Python
In [1]: import test_py
In [2]: timeit -n1 test_py.simple_add(100000000)
1 loops, best of 3: 8.39 s per loop
Python 느리다! C는 아마도 너무 빨라서 측정할수 없다.
그러면 Cython을 사용해보자. pyximport를 사용하면 Cython가 import시에 컴파일 해준다. 먼저 Python의 코드를 변경하지말고 그냥 컴파일 해보자.
Python/pyximport+.py
In [1]: import pyximport; pyximport.install(pyimport = True)
In [2]: import test_py
In [3]: timeit -n1 test_py.simple_add(100000000)
1 loops, best of 3: 4.27 s per loop
아무것도 하지않고 2배정도 빨라졌다! 이것만으로도 만족스러운 결과다.
다음은 역시 코드에 손을 대지 않고 확장자를 Cythonの.pyx로 변경해 보자.
Cython/pyximport+.pyx
In [1]: import pyximport; pyximport.install()
In [2]: import test_cy
In [3]: timeit -n1 test_cy.simple_add(100000000)
1 loops, best of 3: 4.36 s per loop
이전의 결과랑 거의 다르지 않다.
pyx의 형으로 지정하여 실행해보자. cdef의 형을 지정하는 것으로 C의 변수를 사용하므로 빨라 질것이다.
Cython/cdef
In [1]: import pyximport; pyximport.install()
In [2]: import test_cy
In [3]: timeit -n1 test_cy.simple_addc(100000000)
1 loops, best of 3: 1.19 us per loop
・・・( ゚д゚)
Python과 차원이 다르다라고 할까. 놀랄만한 결과다. C의 시간이 time커맨드에서는 측정불능이였는데 이에 비등할정도의 속도?
결과:

time(s)Python과의 상대효율(배)
C측정불능측정불능
Python8.391.0
Python/pyximport+.py4.271.96
Cython/pyximport+.pyx4.361.92
Cython/cdef0.000001197050420

if문과 동적배열

다음은 if문을 사용해 10000000까지의 홀수를 동적으로 배열에 넣는 처리를 해보자
C++는 익숙하지 않지만 vector를 사용해 구현해보았다. Python은 list.append를 사용했다.
C++/vector
$ g++ -O2 odd.cpp
$ time ./a.out

real     0m0.195s
user     0m0.083s
sys     0m0.106s
Python/list
In [1]: import test_py
In [2]: timeit -n1 test_py.odd(10000000)
1 loops, best of 3: 2.24 s per loop
list.append는 있을수 없어! Python을 사용하다면 리스트 내장표기법이 상식! 이라는 말을 하시는 분이 계실것 같아 리스트 내장표기로도 실험해 보자
Python/list내장 표기
In [1]: import test_py
In [2]: timeit -n1 test_py.odd_listcomp(10000000)
1 loops, best of 3: 1.64 s per loop
역시 내장표기법이 확실히 빠르다. 하지만 C++에 비해 느리다
Cythonで은 문제없이 리스트 내장 표기를 사용할수 있으므로 Python의 코드에 형 지정만 더하여 컴파일 한다.
지금부터는 setup.py를 준비해 컴파일을 할것이다.
Cython/cdef+list내장표기
In [1]: import test_cy
In [2]: timeit -n1 test_cy.odd_listcomp(10000000)
1 loops, best of 3: 288 ms per loop
C++보다는 느리지만 원래의 Python보다 현저히 빨라졌다. Cython에서도 리스트 내장표기를 사용할수 있는것이 맘에 든다.
다음은 Cython에서 C++의 vector를 불러내 봤다. 실은 Cython에서는 C++의 표준 라이브러리를 간단하게 사용할수 있도록 되어 있어 deque, list, map, pair, queue, set, stack, vector가 지원되고 있다고 한다. vector<int>가 vector[int]와 같이 조금 문법의 차이가 있지만 Python에서 C++를 적는 느낌이 들었다.
Cython/vector
In [1]: import test_cy
In [2]: timeit -n1 test_cy.odd_vec(10000000)
1 loops, best of 3: 97.4 ms per loop
아니 이럴수가? C++보다 빠르다. C++는 프로그램전체 실행시간을 time커맨드로 측정하고 있는 반면에 Cython은 ipython로 import한후 함수만 불러내므로 유리한 조건이지만 이정도로까지 속도가 좋을줄이야 상상도 하지 못했다.
결과:

time(s)Python과의 상대효율(배)
C++/vector0.19511.5
Python/list.append2.241.0
Python/list내장표기1.641.37
Cython/cdef+list내장표기0.2887.78
Cython/vector0.097423.0

배열에 대한 계산

세번쨰는 NumPy랑도 비교해 보자. 1~10000000의 수를 저장한 배열 전체에서 log를 뽑아 한개씩 읽어 합산을 한다.
NumPy이외는 log와 sum의 계산을 한번 해봐도 좋지만 NumPy에 유리하도록 log와 sum의 일정을 나누어 놓는다. 그리고 만일을 위해 계산결과를 표시한다.
먼저 C++과 Python부터터
C/vector+math.log
$ g++ -O2 logsum.cpp
$ time ./a.out
1.51181e+08

real     0m0.742s
user     0m0.515s
sys     0m0.197s
Python/math.log
In [1]: import test_py
In [2]: li = [i for i in xrange(1, 10000000)]
In [3]: timeit -n1 test_py.logsum(li)
151180949.369
151180949.369
151180949.369
1 loops, best of 3: 3.48 s per loop
다음은 Cython으로 C++와 같이 vector를 사용한다. 또 Python의 math를 사용하지 않고 C의 math.h를 사용하는 쪽이 빠를것 같아 사용한다.
Cython/vector+cdef math.log
In [1]: import test_cy
In [2]: li = [i for i in xrange(1, 10000000)]
In [3]: timeit -n1 test_cy.logsum(li)
151180949.369
151180949.369
151180949.369
1 loops, best of 3: 850 ms per loop
결과가 좋다. C++에 근접한 속도가 나왔다.
다음은 Python으로 행열계산이 장점인 NumPy를 사용해 보자
Python/Numpy
In [1]: import test_py
In [2]: li = [i for i in xrange(1, 10000000)]
In [3]: timeit -n1 test_py.logsum_numpy(li)
151180949.369
151180949.369
151180949.369
1 loops, best of 3: 2.09 s per loop
단순히 Python으로 적는것 보다 빠르지만 Cython/vector의 결과를 봐버리며 
NumPy는 SIMD명령에 대응하고 있으로 더 빠를것이라고 생각했는데 매우 실망했다.
NumPy는 익숙해지면 행열에 대한 계산을 알기쉽게 컴팩트하게 적을수 있지만 속도를 낼려면 Cython으로 적어야 되지 않을까 한다.
결과:

time(s)Python과의 상대효율(배)
C++/vector+math.log0.7424.69
Python/math.log3.481.0
Cython/vector+cdef math.log0.8504.09
Python/NumPy2.091.67

*경품

Cython은 NumPy에도 지원하고 있는듯 하므로 어디까지 고속화 할수 있는건가 실험해보자
10000*10000의 행열에 0부터의 순으로 넣어 모든값에 +1한후 행단위로 sum으로 나누어 행열 전체의 합산을 구한다.
행단위의 합산으로 나누면 행의 합계는 1.0이 될것이다. sum=1*1000이 될것이다.
Python/Numpy
In [1]: import test_py
In [2]: timeit -n1 test_py.dim2sum_numpy(1000)
1000.0
1000.0
1000.0
1 loops, best of 3: 869 ms per loop
Cython/Numpy+cdef int
In [1]: import test_cy
In [2]: timeit -n1 test_cy.dim2sum_numpy(1000)
1000.0
1000.0
1000.0
1 loops, best of 3: 621 ms per loop
Cython은 Python/NumPy의 코드의 루프를 사용하는 변수를 int형으로 지정만 하였다. 극적인 스피드업은 없지만 거의 코드의 수정없이 좋은 결과가 나왔다.
다음은 cimport로 NumPy를 사용하겠다. 이렇게 하면 NumPy의 배열에 고속으로 접근할수있다고 한다.
Cython/cimport Numpy+cdef int
In [1]: import test_cy
In [2]: timeit -n1 test_cy.dim2sum_pynumpy(1000)
1000.0
1000.0
1000.0
1 loops, best of 3: 630 ms per loop
엥? 바뀌지 않았다. cimport로 효과가 있는것은 인수에 NumPy의 배열을 받을때 뿐인가?
음.. 공부부족!
결과:

time(s)Python과의 상대효율(배)
Python/NumPy0.8691.0
Cython/NumPy+cdef int0.6211.4
Cython/cimport NumPy+cdef int6301.38

추기(3/10):
@lucidfrontier45씨의 시간이 바뀌지 않는것은 numpy.ndarray를 cdef로 지정하지 않았기때문이라고 갈켜주셨다.
게다가 원래 ndarray의 배열의 참조는 foo[i][j]보다가도 foo[i,j]쪽이 빠르다는것도 알았으므로 이 두 부분을 수정해 실행해보았다.
결과(수정판):

time(s)Python과의 상대효율(배)
Python/NumPy0.3501.0
Cython/NumPy+cdef int0.2221.58
Cython/cimport NumPy+cdef int0.04318.12
Cython를 사용하지 않아도 2차윈의 ndarray의 참조 방법을 바꾸는 것만으로 빨라 졌다. 이것은 이후 신경쓰지 않으면... 
Cython쪽은 ndarray를 cdef로 하면 매우 빨라지는것을 확인할수 있었다. 이번의 코드만으로 ndarray.sum()를 사용하고 있는 부분이 많지만 배열의 순차 참조부분이 많을수록 차이는 더 커질것이라고 생각한다.
업로드한 소스도 수정했다.

다른분의 벤지마크:

Python을 고속화하는 여러방법들의 벤치마크가 적혀있다.
Python2.7 < PyPy1.5 < Cython < Cython+numpy < ShedSkin
의 순서로 빨랐다고 한다. ShedSkin은 Cython과 동 사양에 Python을 C++로 변환하는 것같은데 아직은 실험단계?

Python < NumPy < Weave < Cython
Weave라는 것은 SciPy의 모듈로써 Python의 코드에 인라인으로 C의 코드를 적을수 있는 것같다.

결론

이번의 결과와 필자의 스킬, 가능성등을 생각하면
Python << NumPy < Cython < Cython+NumPy
*예외로서 호환성을 완전히 배제하고 거의 C의 Cython
이라는 느낌이랄까
NumPy와 Cython을 공부해 가는 동안 순위가 바뀔가능성이 클것 같다.

사용한 소스코드는 github에 업로드했다.


출처 : http://blog.naver.com/parkjy76?Redirect=Log&logNo=30156508008


Articles