PyPy

1 개요

pypy-logo.png
우로보로스

PyPy 프로젝트 사이트
표준 구현인 CPython과의 속도비교. PyPy를 개발하면서 지속적으로 업데이트하고 있다.

2007년에 처음 발표된 Python의 언어 구현 중 하나로, C로 짜여진 기존의 CPython과 달리 Python으로 Python을 만드는 프로젝트이다. 여기까지 얘기하면 뭔가 이상한 짓 하는 프로젝트 내지 실험적인 프로젝트처럼 느껴지겠지만, 이 프로젝트의 진짜 놀라운 점은 기존 CPython에 비해 전혀 느리지 않을 뿐더러, 오히려 성능면에서 CPython을 능가하고 있다는 거다. 심지어 이 링크에서 보이는 대로, 계속해서 빨라지고 있다!

애초에 이 구현은 그저 장난질이나 하려고 시작한 프로젝트가 아니다. Psyco라고 하는, 기존 파이썬 위에다가 Just-In-Time 컴파일을 구현해서 실행성능을 높히는 프로젝트가 있었는데, 이걸 개발하던 Armin Rigo라는 사람이 아예 JIT 컴파일을 하는 파이썬을 처음부터 다시 구현하기로 생각했다. 그래서 2003년부터 PyPy 개발을 시작하여 유럽연합의 연구자금 지원을 받아가며 지금까지 개발하고 있다.

2023-08-03 21:01:27 기준 PyPy3는 윈도우 바이너리가 없으므로 리눅스 바이너리를 실행시켜야 한다. 윈도우10이라면 1주년 업데이트에 포함된 리눅스 배시 기능을 쓰거나, 아니면 윈도우에서 리눅스 어플리케이션을 돌리는 프로그램을 이용할 수도 있다.

2 어떻게 구현하나

기계어컴파일하지 않고 기껏해야 인터프리팅 성능을 빠르게 하기 위해 스크립트만을 쓰는 Python을 가지고 파이썬으로 돌리는 파이썬을 어떻게 만들었을까. PyPy의 접근법은 이렇다.

  1. 먼저 RPython이라고 하는, 파이썬 문법을 엄격하게 만들어 컴파일이 가능하게 만든 해석기(translate.py)를 Python 코드로 작성한다.[1]
  2. RPython의 효과적인 컴파일을 위해 다른 언어로 툴체인을 만든다.[2]
  3. Python 구현(런타임)을 RPython 문법으로 작성한다.
  4. 3에서 만든 구현을 1 또는 2에서 만든 RPython 해석기로 컴파일한다.
  5. 4으로 만든 후보를 이전 또는 다른 구현과 비교(성능 측정), 만족스럽지 않으면 수정한다.(nightly builds)
  6. 5에서 만족스러운 결과를 냈다면 출시하고 1 또는 2부터 다시 시작한다.(release)

뭔가 속는 느낌이지만 처음 한번만 기존 파이썬의 도움을 받으면 그 다음부터는 스스로 만든 구현으로 저 과정을 자체적으로 계속 반복할 수 있다. 이게 얼핏 보면 '닭이 먼저냐 달걀이 먼저냐' 같은 것으로 착각할 수도 있지만, 이런 식의 접근법은 실제로 프로그래밍 언어를 제작할 때 해당되는 사항이며[3], 더 좋은 장비의 개발을 위해 기존의 장비를 사용하는 것과 다름이 없다. 이러한 이유로 로고가 우로보로스인 셈. 다만 PyPy는 문자 그대로 진화하고 있다.

사실 이런 식으로 어떤 언어로 자기 자신을 구현하는 것을 부트스트래핑(Bootstrapping) 또는 부팅(Booting)[4]이라고 하는데, 사실 이렇게 하는 이유는 처음부터 작업하는 것보다 생산성에서 낫기 때문이다. 처음부터 어셈블리어로 작업하면 이론상 최강의 성능을 발휘할 수는 있으나 그 가능성은 매우 낮고 생산성도 최악이다.(최적화가 그래서 힘들다.) 이 때문에 대부분의 프로그래밍 언어도 초창기에는 어셈블리어나 다른 고급언어의 도움을 받지만, 일정 단계를 넘어서면서 그 한계가 오면 스스로의 컴파일러/인터프리터를 제작하게 된다. Haskell 컴파일러인 GHC와 C/C++ 컴파일러인 GCC가 대표적인 예. 물론 생산성에 중점을 두기 때문에 성능이 눈에 띄게 빨라지는 일은 잘 드러나지 않으며, 단지 우연한 발견 등 여러 이유로 성능이 향상되는 것 뿐이다. PyPy의 경우 Python의 한계를 극복하기 위해 정적인 컴파일이 가능한 RPython을 따로 만들었고 뒤에도 언급할 듯 성능 향상을 위해 7년이라는 긴 세월을 소모했는데, 이것도 어셈블리어로 처음부터 하는 것보다는 짧은 것이다.[5]

3 JIT, 그리고 마개조

PyPy는 RPython으로 이루어진 Python 인터프리터와 (역시 RPython으로 이루어진) C, .Net, JVM등을 타게팅한 해석 툴체인으로 이루어져 있는데, 해석 툴체인은 해당 플랫폼을 위한 효율적인 코드를 생성하는 데 초점이 맞춰져 있으며, Tracing JIT을 통해 인터프리터 단위의 JIT 컴파일이 가능하므로, CPython보다 빠른 결과물이 나오게 된다.

여기까지만이어도 그냥 기존 구현에 비해 그다지 느리지 않은 파이썬 구현을 다른 언어의 도움 없이 파이썬 스스로 만든 셈이지만, 여기서 하나가 더 들어간다. Armin Rigo가 뭐 만들던 사람이라고 아까 얘기했던가? 그렇다. PyPy는 JIT compiler다. 그것도 그냥 JIT가 아니라 meta tracing JIT라는 괴랄한 물건을 구현했는데, JIT이 필요한 부분에 약간의 힌트 코드를 넣으면 RPython 해석기가 알아서 JIT compile이 되는 언어 구현을 만들어준다. 다른 언어 구현들이 JIT 좀 해보자고 무지막지한 삽질을 하는 것에 비해[6] 거의 공짜나 다름없다.[7]

하지만, 사실 만들고 나서 보니 그렇게 보이는 거고 PyPy는 그냥 JIT를 구현한 게 아니라 JIT 구현을 만들어주는 컴파일러를 구현한 셈이 되므로 실제 작업은 훨씬 더 어려웠을 것이다. 당장 PyPy가 CPython보다 빨라지게 된 게 PyPy 1.3 이후부터인데 그게 2010년 중순이다.[8] 다시 말해 7년동안 아무도 알아주지 않았던 '진화'가 드디어 빛을 본 셈. 심지어 본인들도 "우리는 영웅이 아니며, 단지 인내심이 많았을 뿐이다."라고 말하고 있다. 잠시 PyPy 개발팀의 근성에 경의를 표하자 (…) 여담으로, 이 녀석의 해석은 사람만 힘든 게 아니라 컴퓨터도 힘든 모양이다. 써볼 사람들은 시간이 남아돌고 사용하는 컴퓨터가 어지간히 고사양이 아니라면 미리 컴파일된 것을 받아 사용하자. CPython을 통한 Bootstrapping에 엄청난 시간과 메모리를 요구한다.[9]

이런 구조로 만든 덕분에 보너스로, RPython 컴파일을 할때 목적 타겟을 다르게 주면 다른 플랫폼에서 돌아가는 파이썬 구현이 나온다. 그래서 똑같은 PyPy 소스코드로 CPython같은 놈도 나오고 Jython같은 놈도 나오고 IronPython같은 놈도 나오고 한다. 추가로 코루틴을 쓰는 Stackless Python같은 것도 비슷한 방법으로 만들 수 있어서 Stackless Python 개발하던 Christian Tismer가 아예 PyPy 개발팀에 와서 같이 일하고 있다. [10] 이쯤되면 뭔가 무섭다

4 아직 해야 할 과제

2016년 7월 현재 최신 버전은 6월에 릴리즈된 PyPy2.7 v5.3PyPy3.3 v5.2 Alpha 이다. 달랑 몇 주 만에 나온 이 버전에서는

  • Python3에 대한 호환이 3.2에서 3.3으로 올라감(단 알파 버전임)
  • Numpy에 대한 호환성 증가
  • Matplot, Scify 컴파일 가능
  • 기타 등등

눈 감으면 성능증가!

PyPy 개발팀의 호환성 기준은 매우 심플해서, 이유 여하를 막론하고 CPython에서 되던 게 PyPy에서 안 되면 그냥 PyPy 버그다. (...) 거의 C 확장모듈 쓴 코드 빼고는 모두 돌아간다고 보면 된다. Python 3는 갈길이 멀다. 추가할 기능에 코루틴이 있는 건 덤.

4.1 기부 모금

현재 PyPy 사이트에서 Python 3.x와의 호환성NumPy 지원인용 오류: <ref></code> 태그를 닫는 <code></ref> 태그가 없습니다 PyPy 측에서는 아예 NumPy를 순수 파이썬 코드로 재작성할 계획을 가지고 있다.</ref>을 위한 기부를 받고 있다. NumPy 쪽은 이미 대부분의 경우 사용에 지장이 없는 수준까지 구현했다고. #[11]

5 예제

심심하면 다음 코드로 테스트 해 보자.

<syntaxhighlight lang="python" line="1">
import random
def populate_doors(): # put a car behind one door
door=['goat', 'goat', 'goat']
door[random.randint(0,2)]='car'
return door
wins = 0
losses = 0

  1. playing the game 100,000 times:

for x in range(100000):
doors=populate_doors()
first_choice=random.randint(0,2) # choose a random door
for y in range(3): # reveal first losing, unchosen door
if doors[y] != 'car' and y != first_choice:
doors[y] = 'out'
break
if doors[first_choice] == 'car':
losses = losses + 1 # contestant switched to losing door
else:
wins = wins + 1 # contestant switched to winning door
print ("All choices were switched.")
print ("Wins:"), wins
print ("Losses:"), losses
</syntaxhighlight>
코드 출처: [1]

PyPy가 대략 25[12] 빠른 것을 알 수 있다. 물론 어떤 코드냐에 따라 배율에 차이가 있고 가끔씩 더 느린 코드도 있지만 어지간한 경우엔 PyPy쪽이 빠르다고 봐도 된다.
  1. RPython는 Python의 일부만 구현한 언어(부분 언어)이기 때문에 모든 RPython 코드는 Python 코드이기도 하다. (역은 성립하지 않는다.)
  2. 시뮬레이터에뮬레이터를 제작할 때에도 이 과정은 반드시 거친다.
  3. 일례로 유명 c/c++언어 컴파일러인 gcc도 c/c++로 작성되었다. 파이썬이 스크립팅 언어이기 때문에 일견 특이해보이는 것일 뿐, 인터프리터는 어짜피 컴파일된 상태로 돌아가므로 같은 원리다.
  4. 운영체제의 부팅과 같은 어원이다.
  5. 컴퓨터 하드웨어의 발전도 마찬가지다. 처음 IC나 LSI를 설계할 때는 레이아웃을 손으로 그렸지만, 지금 나오는 칩들은 손으로 하기에는 회로의 규모가 지나치게 크기 때문에 전부 CAD로 개발하고 있다. 사실 이러한 일은 망치를 만들기 위해 망치를 쓰거나 간석기를 만들기 위해 뗀석기를 썼던 것 처럼 인류의 역사를 대표하는 방법론 중 하나이므로 전혀 속는 느낌 가질 거 없다.
  6. 인터프리트나 바이트코드 컴파일 방식을 취하는 언어 구현 중에 JIT를 실용적으로 하는 구현은 JVM이나 .NET CLR 정도밖에 없다. 돈 좀 쓰고 다닐 것 같은 느낌이 들지 않는가?
  7. PyPy가 대체 무슨 마개조를 벌이는지 더 자세히 알고 싶으면 이 글을 참고하자. 뭔가 여기하고 많이 비슷해 보이는 건 착각......이 아닐 거다. 이 위키 항목의 작성에 상당부분을 참조했다 (...)
  8. 위에서도 얘기했지만 2003년에 개발을 시작했다.
  9. 준비물이 모두 갖춰진 상태에서 PyPy 2.2.1 달랑 하나를 빌드하는 데에 모질라 파이어폭스와 그것이 의존하는 모든 다른 패키지들을 빌드하는 것보다 시간이 더 걸린다! Core i7 3.33GHz, 다른 작업 방해 없이 코어 1개 풀사용, 8GB 램 기준으로 최소 3시간은 각오해야 한다. 64비트 CPython으로 컴파일하다 보면 Python 프로세스 하나가 램을 5GB 가까이 처묵처묵하는 것도 볼 수 있다. Garbage collection 따위는 장식입니다 PyPy로 PyPy를 빌드해보신 분이 있으면 추가바람.
  10. 여담이지만 이 두 사람이 모여서 만든 또 하나의 작품이 greenlet인데, Psyco처럼 기존의 CPython에다가 마개조를 해서 원래 지원 안 되는 코루틴을 만들어 올리는 라이브러리다... 참고. 추가적으로 말하자면 코루틴은 파이썬 3.4부터 내장되며 3.5부터는 async과 await이라는 키워드도 탑재된다.
  11. numpy관련 기부 모금은 현재 닫힌 상태이다.Call for donations - PyPy to support Numpy!
  12. Python 2.7.11 x86_64, PyPy 5.1.0 x86_64 기준.