2011년 5월 2일 Romantic Binaries: PyPy가 CPython보다 빠를 수 있는 이유
PyPy 1.5가 나왔다. Python 프로그래머에게 있어서 이번 릴리즈의 가장 큰 의의는 아마 Python 2.7.1 호환성을 제공한다는 데에 있을 것이다. (이전 버전은 2.5 호환성을 제공했다. 반년도 안되어서 2.5 → 2.7.1로 호환성을 올린 것이니 참으로 괴물 같은 개발 속도라 할 수 있다.)
해외에서도 그렇고 LangDev 채널에서도 그렇고 PyPy에 관한 가장 흔한 질문은 이것이다: PyPy는 Python으로 Python을 구현한 건데 어떻게 CPython보다 빠르다는 겁니까?
[후략]
PyPy가 취하는 접근 방법을 meta-tracing JIT라고 한다. 이건 두 종류의 접근 방법을 확장한 건데, 보통 JIT 컴파일을 할 때는 코드가 실행되기 얼마 전에 코드를 보고 네이티브 코드로 컴파일을 하고, tracing JIT에서는 보통 코드는 인터프리터로 돌리다가 많이 실행된다 싶으면 코드 실행 기록(trace)을 보고 네이티브 코드로 컴파일한다. (따라서 tracing JIT는 보통 JIT에 비해 처음에는 느리다가 나중에는 빨라지는 경우가 적잖다. 자주 실행되는 코드에 어떤 제약이 주로 가는지를 파악해서 더 좋은 코드를 생산할 수 있기 때문에.)
그런데 meta-tracing JIT에서는 tracing을 하기는 하는데 그 범위가 인터프리터 단위이다. 이게 뭔 말이냐 하면, 일반적인 JIT를 추가하려면 인터프리터를 완전히 날리거나(non-tracing JIT의 경우) 인터프리터 사이에 JIT 코드를 넣고 JIT된 네이티브 코드에서 세웠던 가정이 깨지면 다시 인터프리터로 돌아 오는 등의 삽질(tracing JIT의 경우)이 필요하다. 당연히 인터프리터가 복잡하면 JIT를 넣는 것 자체가 머리 아프고, 인터프리터를 새로 짤 때는 갖다 쓸 수 없다는 단점이 있다. 그러나 meta-tracing JIT에서는 인터프리터 자체를 JIT 컴파일해 버리기 때문에 인터프리터는 인터프리터 대로 고치고 JIT는 JIT 대로 고쳐도 서로에게 영향을 미치지 않게 할 수 있다는 매우 큰 장점이 있다. 물론 이걸 무식하게 구현하면 인터프리터의 비효율적인 바이트코드 루프가 최적화가 덜 된 채로 JIT 컴파일이 되는 경우가 있을 수 있으나, PyPy의 경우 힌트를 적절히 줘서 최적화를 “잘” 할 수 있도록 하는 기능을 제공하고 있으므로(이게 JIT를 인터프리터와 통합하는 것보다는 싸게 먹힌다) 이 문제를 피할 수 있다.
따라서 PyPy가 개발에 그토록 오래 걸렸던 이유는, 인터프리터가 복잡해서가 아니라 모든 종류의 인터프리터를 비교적 잘 돌릴 수 있는 meta-tracing JIT를 만드는 데 엄청난 시간이 걸렸기 때문이다. 아니, 좀 더 정확하게 말하면, PyPy는 현존하는 meta-tracing JIT 구현 중 가장 성숙하고 상용화 단계에 접어든 괴물이라고 말할 수 있다(연구 단계에 있는 구현은 몇 개 있는 것 같다). 일단 한 번 JIT 엔진을 잘 만들면 인터프리터를 (비교적) 고수준 언어인 RPython으로 짜서 빠르게 돌릴 수 있다는 것이 PyPy의 궁극적인 목표인데, 아직까지 파이썬 말고 다른 언어가 진지하게 구현된 경우는 프롤로그(…) 밖에 없지만 앞으로 PyPy의 툴체인이 성숙하면 뭔가 더 나오지 않을까 기대가 되는 바이다.
