洪民憙 (홍민희) 블로그

이하의 글은 2011년에 쓴 것입니다. 오래된 글인 만큼, 현재의 생각과 전혀 다른 내용도 많이 포함되어 있고, 당시와는 상황이 많이 달라진 점도 있습니다. 또한, 그 당시에 잘못 알려졌던 정보도 포함되어 있을 수 있습니다. 어찌됐든 저는 제 오래된 글이 회자되는 것을 저어합니다. 읽기에 앞서 양해를 부탁드립니다.

PyPy가 CPython보다 빠를 수 있는 이유

PyPy 1.5가 나왔다. Python 프로그래머에게 있어서 이번 릴리즈의 가장 큰 의의는 아마 Python 2.7.1 호환성을 제공한다는 데에 있을 것이다. (이전 버전은 2.5 호환성을 제공했다. 반년도 안되어서 2.5 → 2.7.1로 호환성을 올린 것이니 참으로 괴물 같은 개발 속도라 할 수 있다.)

해외에서도 그렇고 LangDev 채널에서도 그렇고 PyPy에 관한 가장 흔한 질문은 이것이다: PyPy는 Python으로 Python을 구현한 건데 어떻게 CPython보다 빠르다는 겁니까?

이것은 매우 자연스러운 질문이다. 비유하자면 가상 머신 안에서 돌아가는 프로그램이 실제 물리적인 컴퓨터에서 바로 실행되는 프로그램보다 빠르다는 얘기와 비슷한 소리 아닌가? 쉽게 생각하면 말이 안되는 이야기다. 하지만 C 컴파일러는 C로 작성되며, C 컴파일러를 컴파일하는 데에 사용한 컴파일러보다 컴파일된 C 컴파일러가 빠를 수 있다는 것을 생각해보면 PyPy가 CPython보다 빠른 것도 가능한 이야기다.

PyPy는 단순히 Python으로 구현한 Python으로 널리 알려져 있지만, 실제로 PyPy가 하는 일은 크게 두 가지이다.

  1. RPython 언어로 동적 언어의 인터프리터를 구현할 수 있게 해주는 프레임워크
  2. RPython으로 구현된 Python

1은 Python으로 구현되었고 따라서 CPython이나 PyPy로 실행 가능하다. 2는 말 그대로 RPython으로 구현되어 있다.

RPython이 무엇이냐? 정적 컴파일이 가능하도록 Python 언어에 제약을 가한 Python의 부분 집합 언어이다. 컴파일하는 데에 장애가 되는 Python의 동적인 기능을 제거한 것으로 생각하면 된다. 그리고 PyPy는 RPython을 C나 JVM 바이트 코드, CLI IL로 번역하는 컴파일러(translate.py)를 구현하고 있다. 이게 1번이다.

그리고 RPython으로 Python 언어의 인터프리터를 구현했다. 이 인터프리터는 translate.py를 이용해 C나 JVM 바이트코드 등으로 번역된다. 백엔드를 JVM으로 하면 Jython 같은 것이 나오고 CLI IL로 하면 .NET에서 돌아가는 IronPython 같은 것이 떨어지며, C로 하면 CPython 같은 것이 떨어진다는 이야기다. 그리고 --withmod-_stackless 옵션을 주면 Stackless Python 같은 것이 떨어진다. 그야말로 인터프리터 찍어내는 프로그램이라고 생각하면 정확히 이해하는 거다. (여기서 중요한 것은 인터프리터 구현 소스는 백엔드 타겟과 중립적으로 하나만 존재한다는 점이다.)

여기까지 이해했다면 PyPy가 Python에 비해 심각할 정도로 느리지는 않을 거라고 예상할 수 있다. 인터프리터로 인터프리터를 돌리는 것이 아니라 인터프리터를 정적으로 컴파일하는 것이기 때문이다. 그렇다면 PyPy는 어떻게 실질적인 스피드업을 해낼까?

바로 여기에 PyPy의 외계 기술이 들어간다. translate.py는 RPython으로 구현한 인터프리터 소스 코드에 루프가 어디에서 시작해서 어디에서 끝나는지 정도만 annotation을 추가하면 알아서 그냥 Tracing JIT를 인터프리터에 끼워준다!!! 즉 RPython으로 Lua 인터프리터를 구현하면 LuaJIT 같은 것이 나오는 것이고 JavaScript 인터프리터를 구현하면 TraceMonkey 같은 것이 나오는 것이다! 이것은 굉장히 놀라운 기술인데, JIT를 추가하기 위해서는 언어 구현에 전역적으로 JIT 관련 코드가 침투해야 한다는 점을 생각해보면 공짜도 이런 공짜가 없는 것이다.1

자, 이론적으로는 빠르지만 실제로는 얼마나 빠를까? 궁금하다면 PyPy Speed Center를 확인하면 된다. PyPy는 매 릴리즈 성능을 올리고 있으며, 아직도 더 올라갈 여지가 많다. 물론 이미 CPython보다 대부분의 벤치마크에서 빠른 상태다.

성능만 좋으면 대수냐? 원래 CPython에서 돌아가던 Python 소프트웨어가 잘 돌아가야 의미가 있지 않느냐? 물론 잘 돌아간다. 어제도 내가 만들었던 Flask, Werkzeug, Jinja2, SQLAlchemy 등에 의존하는 웹 애플리케이션 하나를 PyPy에서 코드를 전혀 수정하지 않고 돌려봤다. 모두 잘 돌아간다. :)

호환성 이슈는 PyPy에게 매우 중요하다. CPython을 대체해야 하기 때문이다(!). 레거시 코드를 수정 없이 제대로 동작시킬 수 있어야 한다. 그래서 PyPy의 호환성 정책은 애초에 이렇다: CPython에서 돌아가던 게 PyPy에서 안 돌아가면 PyPy의 버그다. 그런 게 있다면 주저없이 PyPy 이슈트래커에 신고하면 된다.

호환성의 큰 장애가 되던 것은 C 확장들인데 그것마저 CPyExt 덕분에 대부분 해결된 상태이다. 몇몇 많이 사용되는 소프트웨어는 PyPy 팀이 아예 나서서 호환성 패치를 작성하기도 한다.

이렇게 훌륭한 프로젝트가 어디 있을까! PyPy는 놀라운 외계 기술을 선보이고 있지만 물론 하루아침에 이루어진 게 아니다. 지금은 GvR조차 Python의 차세대 레퍼런스 구현으로 PyPy를 지목하고 있을 정도이지만, 사실 이 프로젝트는 거의 10년 가까이 진행된 끝에 최근에 와서야 그 결실을 보고 있는 것이다. 이와 관련해서는 작년 말 PyPy 블로그에 올라온 우리는 영웅이 아니라 그저 인내력이 매우 강했을 뿐이라는 글을 읽어보면 된다.

그래서 이 글의 요점은? 당장 PyPy를 받아서2 CPython 대신 써보자는 얘기이다. :-)

추가. 강성훈 씨가 이 글과 관련해 meta-tracing JIT에 대해 좀더 자세한 글을 썼으니 읽어보면 좋다.


  1. 물론 아직까지는 이러한 meta-tracing이 일반적으로 손으로 구현되는 tracing에 비해 효율이 좀 떨어지기는 한다. 하지만 구현의 복잡도를 생각한다면 장기적으로는 meta-tacing이 더 나은 방향일 수 있다. 강성훈 씨의 의견 감사.

  2. 궁금한 사람은 RPython으로 작성된 Python 인터프리터를 translate.py로 번역해서 직접 빌드하고 싶겠지만, 매우 time-consuming + memory-hungry한 절차이므로 그냥 바이너리를 받아서 쓰길 추천한다.