May 02, 2020
이번 글에서는 Mac OS에서 PyPy 설치 방법과 PyPy 가상 환경을 생성하는 방법을 소개하고 간단하게 CPython 과의 성능을 비교해 보고자 한다.
Homebrew가 설치되어 있다면 PyPy 설치 방법은 간단하다.
brew install pypy3
# 실행해보기
pypy3 --version
Python 3.6.9 (?, Apr 18 2020, 02:46:07)
[PyPy 7.3.1 with GCC 4.2.1 Compatible Apple LLVM 11.0.3 (clang-1103.0.32.59)]
현재 날짜(2020.05.02) 기준 최신 버전인 PyPy 7.3.1 버전과 CPython과의 버전 호환성
가장 최신 정보는 여기서
CPython | Compatibility |
---|---|
3.8 | x |
3.7 | x |
3.6 | O |
2.7 | O |
평소에 virtualenv를 사용해보신 분들은 당황할 수도 있을만큼 일반 Python과 다를 게 없다. 아래와 같이 하면 된다.
# virtualenv가 깔려있지 않다면
brew install virtualenv
# PyPy 실행 경로 찾기
which pypy3
${PyPy 실행 경로}
# PyPy 가상 환경 만들기
virtualenv -p ${PyPy 실행 경로} pypy-venv
# 가상 환경 실행하기
source ./pypy-test/bin/activate
# 생성한 가상 환경의 실행 파일인지 확인
# 위의 실행 경로와 같은 값이 나온다면 잘못된 것임
# 가상 환경 디렉토리 삭제 후 재시도 ... 안 되면 구글링을 ...
which pypy3 # or which python
PyPy(7.3.1)과 CPython(3.8)의 성능을 비교해보자. 소소하게 5억까지의 합을 2번 구하는 코드로 테스트해보려고 한다.
from timeit import default_timer as timer
def cpu_bound(n):
return sum(range(n))
start = timer()
numbers = [500000000] * 2
for number in numbers:
cpu_bound(number)
print(timer() - start)
pypy single_thread_cpu_bound.py
1.833684525998251
python single_thread_cpu_bound.py
24.585984577999998
from concurrent.futures import ThreadPoolExecutor
from timeit import default_timer as timer
numbers = [500000000] * 2
def cpu_bound(n):
return sum(range(n))
with ThreadPoolExecutor(2) as executor:
start = timer()
for fs in executor.map(cpu_bound, numbers):
pass
print(timer() - start)
pypy multi_thread_cpu_bound.py
2.0052164450025884
python multi_thread_cpu_bound.py
25.727305493
from concurrent.futures import ProcessPoolExecutor
from timeit import default_timer as timer
numbers = [500000000] * 2
def cpu_bound(n):
return sum(range(n))
if __name__ == "__main__":
with ProcessPoolExecutor(2) as executor:
start = timer()
for fs in executor.map(cpu_bound, numbers):
pass
print(timer() - start)
pypy multi_process_cpu_bound.py
0.9554126810035086
python multi_process_cpu_bound.py
11.669450799
옛날 비교글들을 보면 CPython이 거의 40배 이상 느렸는데 현재는 20배 정도밖에(?) 차이가 안 나는 걸 확인할 수 있었고, 멀티 쓰레드/프로세스 환경에서 GIL의 의한 성능의 변화들은 비슷했다. PyPy의 숫자가 작아서 변화량이 적은 거 같았는데 비율상 비슷했다. I/O bound 프로그램은 PyPy나 CPython의 성능과 함께 I/O의 성능의 차이가 결과에 반영될 가능성이 있다고 생각해서 CPU Bound 프로그램만 테스트했다. 이 실험에서는 PyPy가 성능면에서 압도적으로 우위에 있지만 어떤 상황이냐에 따라서 CPython이 더 나은 경우도 있기 때문에(예를 들면, 최신 버전 CPython을 사용하는 모듈을 사용해야 하는 경우) 무조건 PyPy를 써야만 하는 것은 아니라고 생각한다.
GIL과 멀티 쓰레드, 프로세스 대해서 궁금한 점은 What is the Python Global Interpreter Lock (GIL)?, Speed Up Your Python Program With Concurrency 두 글을 읽어보면 좋을 것 같다.
PyPy 설치부터 간단한 테스트까지 마무리해보았다. 설치는 이래도 되나? 싶을 정도로 간단했지만 최신 버전의 CPython과 호환이 되지 않는 점이 아쉬웠다. 뭐..PyPy의 설계상 어쩔 수 없는 부분이긴 하지만 그래도 성능 부분에서 PyPy가 월등하게 앞서는 걸 보고 실제 프로젝트에서 적용해보는 것도 고려해봐야겠다는 생각이 들었다.
PyPy가 왜 더 빠른지, 어떤 방식으로 동작하는지는 PyPy 개발 문서를 한 번 읽어보면 좋을 거 같다. 읽기 귀찮으신 분들을 위해 요약하자면, RPython을 이용해서 Python 인터프린터를 구현하고 Tracing JIT 컴파일러를 인터프린터에 적용한 것이 PyPy이다. 간단하게 말해서 그렇지 7년이라는 시간을 투자해서 이뤄낸 결과이다… 포기하지 않고 성과를 이뤄낸 PyPy 개발팀의 인내력에 감탄했다.
실수를 통해서 알게된 것이 있는데 바로 PyPy에서는 멀티 프로세스 환경에서도 아래와 같이 메인 프로세스에서만 실행하는 코드를 추가하지 않아도 된다는 점이다. 같은 코드를 CPython에서 돌렸는데 오류가 나서 아차 싶었는데 생각해보니 PyPy에서는 잘 실행이 됐었다. 아마도 컴파일 과정에서 이런 부분도 최적화하는 부분이 있는 것 같다.
# CPython에서는 필수
if __name__ == "__main__":
# do something