JIT 컴파일러 생성의 동기

이 글은 RPython이 JIT 컴파일러를 생성하는 접근 방식을 비기술적으로 소개하고 그 동기를 설명합니다.

동기

개요

Python처럼 복잡한 동적 언어의 인터프리터를 작성하는 일은 결코 작은 작업이 아닙니다. 특히 성능 목표를 위해 Just-in-Time(JIT) 컴파일러까지 작성하려 한다면 더욱 그렇습니다.

다행히도 저희는 그렇게 하지 않았습니다. 저희는 실제로 Python 인터프리터를 작성했지만, PyPy에서 Python용 JIT 컴파일러를 작성한 적은 한 번도 없습니다. 그 대신 저희는 Python 인터프리터가 훌륭한 고수준 언어인 RPython으로 작성되어 있다는 점을 활용하여, 이 인터프리터를 자동으로 Python용 JIT 컴파일러로 바꿉니다.

물론 이 변환은 사용자, 즉 Python 프로그램을 작성하는 프로그래머에게는 완전히 투명합니다. 우리의 목표는 (실제로 달성했듯이) 예를 들어 임의의 프레임 접근과 디버거를 포함하여 모든 Python 기능을 지원하는 것입니다. 하지만 이 변환은 언어 구현자, 즉 Python 인터프리터의 소스 코드에도 대부분 투명합니다. 약간의 안내만 있으면 됩니다. 우리는 인터프리터의 소스 코드에 소수의 힌트만 넣으면 되었습니다. 이러한 힌트를 바탕으로 JIT 컴파일러 생성기는 구조상 원래의 인터프리터와 동일한 언어 의미론을 가지는 JIT 컴파일러를 생성합니다. 이 JIT 컴파일러 자체는 런타임에 기계어 코드를 생성하며, 의미론을 변경하지 않으면서 사용자의 프로그램을 적극적으로 최적화하여 큰 성능 향상을 이끌어 냅니다. 물론 흥미로운 점은 우리의 Python 언어 인터프리터가 JIT 컴파일러와 어긋나지 않으면서 시간이 지남에 따라 발전할 수 있다는 것입니다.

우리가 걸어온 길

이전 버전의 PyPy JIT 생성기들은 부분 평가(partial evaluation)에 기반했습니다. 부분 평가는 잘 알려져 있고 많이 연구된 주제로, 매우 유망하다고 여겨집니다. 부분 평가를 이용해 인터프리터를 컴파일러로 자동 변환하려는 시도가 많이 있었습니다. 그러나 그중 어느 것도 실제 언어에서 상당한 속도 향상으로 이어지지 못했습니다. 저희는 지금까지 놓치고 있던 핵심 통찰이 부분 평가를 고전적인 AOT(ahead-of-time) 컴파일러가 아니라 JIT 컴파일러를 만드는 데 사용하는 것이라고 생각합니다. 이것이 옳다고 판명된다면 동적 언어의 실제 속도가 크게 향상될 수 있습니다.

이전의 JIT 컴파일러 생성기들은 모두 손으로 작성한 Psyco와 비슷한 JIT 컴파일러를 생성했습니다. 그러나 2009년부터 현재까지 저희 프로토타입은 더 이상 부분 평가(partial evaluation)를 사용하지 않습니다 – 적어도 논문 심사자를 납득시킬 만한 방식으로는 사용하지 않습니다. 그 대신 최근 Java와 JavaScript를 대상으로 연구된 트레이싱 JIT이라는 개념에 기반합니다. 하지만 지금까지 존재하는 모든 트레이싱 JIT과 비교하면, 부분 평가는 이전 JIT 생성기에 이미 갖추고 있던 몇 가지 추가 기법을 제공하며, 특히 할당을 제거하여 구조체를 최적화하는 방법이 그렇습니다.

현재 저희 JIT와 가장 가까운 비교 대상은 Tamarin의 TraceMonkey입니다. 그러나 이 JIT 컴파일러는 수작업으로 작성되었으며, 이는 상당한 노력이 드는 일입니다. 대신 저희는 RPython 수준에서 JIT 생성기를 작성합니다. 이는 최종 JIT가 완전한 Python 언어의 모든 세부 사항을 담도록 작성될 필요가 없다는 것, 아니 사실은 그렇게 작성될 수조차 없다는 것을 의미합니다. 이러한 세부 사항은 저희가 완전한 Python을 위한 인터프리터를 갖추고 있다는 사실 덕분에 자동으로 제공됩니다.

실용적 결과

저희가 생성하는 JIT 컴파일러는 지금까지 널리 쓰이지 않은 몇 가지 기법을 사용하지만, 그렇다고 완전히 새로운 기법도 아닙니다. 여기서 말하고자 하는 바는 특정 동적 언어를 얼마나 빠르게 실행할 수 있는지에 대한 이론적 한계를 저희가 넓히고 있다는 것이 아닙니다. 저희가 말하고자 하는 바는 이것입니다. 저희는 아무리 복잡하거나 널리 쓰이지 않는 언어라도(예: 대규모 산업계나 학계의 지원이 없는 오픈 소스 동적 언어, 또는 내부 도메인 특화 언어) 모든 동적 언어가 꽤 좋은 JIT 컴파일러를 갖추는 것을 실용적으로 가능하게 만들고 있습니다. 여기서 실용적이라는 말은 이 일이 다음과 같아야 한다는 뜻입니다:

  • 쉬움: 애초에 인터프리터를 작성하는 데 드는 노력에 비해 약간의 노력만 더 들이면 됩니다.
  • 유지 보수 용이성: 저희가 생성하는 JIT 컴파일러는 별도의 프로젝트가 아닙니다(별도의 소스 코드를 생성하지 않고, 생성된 VM에 함께 컴파일되는 일회용 C 코드만 생성합니다). 다시 말해, 고수준 인터프리터가 수정될 때마다 JIT 컴파일러 전체가 처음부터 새로 생성되므로, 언어가 아무리 빠르게 발전하더라도 둘이 서로 어긋날 수 없습니다.
  • 충분히 빠름: 생성된 JIT 컴파일러로 상당히 좋은 성능을 얻을 수 있습니다. 물론 그것이 바로 핵심입니다.

속도 향상을 위한 대안적 접근 방식

NOTE:Please take the following section as just a statement of opinion. In order to be debated over, the summaries should first be expanded into full arguments. We include them here as links; we are aware of them, even if sometimes pessimistic about them :-)

동적 프로그래밍 언어의 실행 속도를 향상시키는 접근 방식은 매우 많습니다. 그러나 그 대부분은 작은 개선만을 가져올 뿐이며, 저희의 접근 방식이 제공하는 유연성과 사용자 정의 가능성을 제공하는 것은 하나도 없습니다. 지난 6년간 미세 조정을 거쳤지만, CPython의 속도는 (벤치마크에 따라) 1.3배 또는 1.4배 향상되는 데 그쳤습니다. 많은 미세 조정은 PyPy에도 적용할 수 있습니다. 실제로 CPython의 미세 조정 중 일부는 원래 PyPy를 위한 미세 조정으로 시작되었습니다.

IronPython은 처음에 언어의 일부 세부 사항을 생략하고, Microsoft가 .NET 플랫폼을 빠르게 만드는 데 쏟아부은 막대한 투자를 활용하여 CPython의 약 1.8배에 달하는 속도를 달성했습니다. 그러나 더 완전해진 현재의 구현은 CPython과 거의 같은 속도를 냅니다. 전반적으로 기존의 접근 방식들은 속도 면에서 한계에 도달했습니다. 이와 관련해 자주 언급되는 Microsoft의 동적 언어 런타임(Dynamic Language Runtime, DLR)은 본질적으로 IronPython에서 처음 도입된 기법을 공식화하기 위한 API에 지나지 않습니다. 기껏해야 또 한 번의 소폭 개선을 가져다줄 뿐입니다.

자주 언급되는 또 다른 기법은 속도를 높이기 위해 언어에 타입을 추가하는 것입니다. 명시적인 선택적 타이핑(optional typing)을 도입하거나 소프트 타이핑(soft typing), 즉 추론된 “개연성 있는” 타입을 사용하는 방식입니다. Python의 경우 이 분야의 모든 프로젝트는 언어의 단순화된 부분집합에서 출발했으며, 언어 전체에 근접한 수준까지 확장된 프로젝트는 하나도 없습니다. 이는 막대한 노력이 드는 일이며, 플랫폼과 언어에 종속적인 작업이 될 것입니다. 게다가 유지보수도 골칫거리가 될 것입니다. CPython에서는 간단히 구현할 수 있는 많은 변경 사항이 이전에 공들여 조정한 최적화를 무효화할 가능성이 높다고 생각합니다.

속도를 크게 향상하려면 JIT 기법이 필요합니다. Python의 경우 Psyco는 일반적으로 2~4배의 속도 향상을 제공하며, 알고리즘 예제에서는 최대 100배까지 빨라집니다. Psyco는 개발과 유지 보수에 따르는 어려움과 막대한 비용 때문에 막다른 길에 이르렀습니다. Psyco는 언어 의미론을 비교적 빈약하게 인코딩합니다. Python 동작에 관한 지식을 손으로 직접 인코딩하고 최신 상태로 유지해야 합니다. 그래도 Psyco는 지원하지 않는 수많은 Python 구문 중 하나를 만나더라도 CPython으로 대체 실행하므로 올바르게 동작합니다. PyPy JIT는 메타프로그래밍 방식이며 특정 언어에 종속되지 않는 Psyco의 대응물로 시작했습니다.

이와는 다른 종류의 선행 기술로는 Jikes와 같은 셀프 호스팅 JIT 컴파일러가 있습니다. Jikes는 Java로 작성된 Java용 JIT 컴파일러입니다. Jikes는 언어 의미론을 제대로 인코딩하지 못합니다. Python과 같은 언어의 세부 사항을 모두 JIT 컴파일러에 직접 인코딩하려면 엄청난 양의 작업이 필요할 것입니다. 또한 이식성이 제한적이라는 점도 Python에는 문제가 됩니다. 원래 의도한 저수준 환경이 아닌 다른 환경에서 실행하려면 JIT 컴파일러의 상당 부분을 리타게팅(retargetting)해야 할 가능성이 큽니다.

JVM의 JIT처럼 이미 잘 튜닝된 기존 JIT를 단순히 재사용하는 방식은 구현자의 언어와 호스트 VM 언어 사이의 개념 불일치 때문에 제대로 동작하지 않습니다. 전자는 JIT가 그것을 크게 가속할 수 있는 방식으로 대상 환경에 맞게 컴파일되어야 하는데, 이 접근법은 지금까지 Python에서 사실상 실패했습니다. CPython은 단순한 인터프리터임에도 불구하고, 이를 Java와 .NET으로 재구현한 것들은 그다지 더 빠르지 않습니다.

최근에는 JIT 분야에서 더 큰 규모의 프로젝트 몇 개가 시작되었습니다. 그러한 사례로, Sun Microsystems는 Java Hotspot JIT를 사용해 Ruby의 성능을 개선하는 것을 목표로 하는 JRuby에 투자하고 있으며, 이는 하나의 인스턴스에 해당합니다. 그러나 이 방식은 많은 수작업을 필요로 하며, 하나의 플랫폼에서 하나의 언어에 대해서만 속도 향상을 제공할 것입니다. 동적 언어에서 흔히 나타나는 끊임없는 박싱과 언박싱의 오버헤드를 어떻게 제거할 것인가와 같이 까다로운 문제들도 있습니다. PyPy와 비교했을 때의 장점은 메타프로그래밍 접근 방식에는 맞지 않는 몇몇 수작업 최적화를 수행할 수 있다는 점입니다. 그러나 메타프로그래밍은 PyPy JIT를 다양한 실행 플랫폼에서 다양한 언어에 재사용할 수 있게 해 줍니다. 두 접근 방식을 결합하는 것도 가능합니다. 우리의 JIT를 사용해 상당한 속도 향상을 얻은 다음, 그 결과를 Java의 Hotspot JIT에 넘겨 추가로 개선할 수 있습니다. 우리 중 한 명은 동적 언어를 더 잘 지원하도록 JVM에 추가할 사항을 정의하는 JSR 292 전문가 그룹의 일원이기도 하며, 우리의 JIT 연구에서 얻은 통찰을 PyPy에도 도움이 되는 방식으로 제공하고 있습니다.

마지막으로, 현재 TraceMonkey를 통해 JavaScript와 같은 동적 언어에서도 트레이싱 JIT가 등장하고 있습니다. PyPy가 생성하는 코드는 (손으로 작성한 것은 아니지만) 트레이싱 JIT의 개념과 매우 유사합니다.

더 읽을거리

현재 RPython JIT 생성기에 대한 설명은 PyJitPl5 (초안)에 있습니다.