JIT 컴파일러 생성의 동기¶
이는 Just-In-Time 컴파일러 생성에 대한 RPython의 접근 방식을 다루는 비기술적인 소개와 그 동기에 대한 설명입니다.
동기¶
개요¶
Python과 같은 복잡한 동적 언어를 위한 인터프리터를 작성하는 일은 결코 작은 작업이 아닙니다. 특히 성능 목표를 위해 Just-in-Time(JIT) 컴파일러까지 작성하고자 한다면 더욱 그렇습니다.
좋은 소식은 그것이 저희가 한 일이 아니라는 것입니다. 저희는 실제로 Python을 위한 인터프리터를 작성했지만, PyPy에서 Python을 위한 JIT 컴파일러는 전혀 작성하지 않았습니다. 대신, 저희는 Python용 인터프리터가 훌륭하고 고수준인 언어인 RPython으로 작성되어 있다는 사실을 활용하며 – 이를 자동으로 Python용 JIT 컴파일러로 바꿉니다.
이 변환은 물론 사용자, 즉 Python 프로그램을 작성하는 프로그래머에게 완전히 투명합니다. 목표(우리가 달성한 목표)는 예를 들어 임의 프레임 접근과 디버거를 포함하여 Python의 모든 기능을 지원하는 것입니다. 하지만 이는 언어 구현자, 즉 Python 인터프리터의 소스 코드에도 대체로 투명합니다. 약간의 안내만 필요할 뿐입니다. 저희는 인터프리터의 소스 코드에 소수의 힌트를 넣어야 했습니다. 이 힌트들을 바탕으로 JIT 컴파일러 생성기는 구조적으로 원래 인터프리터와 동일한 언어 시맨틱스를 가진 JIT 컴파일러를 생성합니다. 이 JIT 컴파일러 자체는 런타임에 기계어 코드를 생성하며, 시맨틱스를 변경하지 않으면서도 사용자의 프로그램을 적극적으로 최적화하여 큰 성능 향상을 이끌어냅니다. 물론 흥미로운 점은 저희의 Python 언어 인터프리터가 JIT 컴파일러와 어긋나지 않으면서도 시간이 지남에 따라 발전할 수 있다는 것입니다.
우리가 걸어온 길¶
PyPy의 JIT 생성기의 이전 버전들은 부분 평가를 기반으로 했습니다. 이는 잘 알려져 있으며 많은 연구가 이루어진 주제로, 매우 유망한 것으로 여겨지고 있습니다. 이를 이용해 인터프리터를 컴파일러로 자동 변환하려는 시도가 많이 있었습니다. 그러나 그중 어느 것도 실제 언어에서 상당한 속도 향상으로 이어지지는 못했습니다. 저희는 빠져 있던 핵심 통찰이 전통적인 사전 컴파일러가 아니라 부분 평가를 이용해 JIT 컴파일러를 만들어내는 것이라고 믿습니다. 만약 이것이 옳다고 판명된다면, 동적 언어의 실제 속도가 크게 향상될 수 있습니다.
이전의 이러한 JIT 컴파일러 생성기들은 모두 수작업으로 작성된 Psyco와 유사한 JIT 컴파일러를 만들어냈습니다. 하지만 2009년부터 시작된 오늘날, 저희의 프로토타입은 더 이상 부분 평가(partial evaluation)를 사용하지 않습니다 – 적어도 논문 심사자들을 설득할 수 있는 방식으로는 사용하지 않습니다. 그 대신 이는 Java와 JavaScript를 대상으로 최근 연구된 트레이싱 JIT이라는 개념에 기반하고 있습니다. 하지만 지금까지 존재하는 모든 트레이싱 JIT과 비교했을 때, 부분 평가(partial evaluation)는 이전의 JIT 생성기에서 이미 보유하고 있던 몇 가지 추가적인 기법—특히 할당(allocation)을 제거함으로써 구조체를 최적화하는 방법—을 저희에게 제공해 줍니다.
현재 저희의 JIT과 가장 가까운 비교 대상은 Tamarin의 TraceMonkey입니다. 하지만 이 JIT 컴파일러는 수작업으로 작성되었으며, 이는 상당한 노력이 필요합니다. 대신, 저희는 RPython 수준에서 JIT 생성기를 작성하며, 이는 최종 JIT이 완전한 Python 언어의 모든 세부 사항을 인코딩하도록 작성될 필요가 없다는—사실, 그럴 수도 없다는—것을 의미합니다. 이러한 세부 사항은 저희가 완전한 Python용 인터프리터를 갖고 있다는 사실에 의해 자동으로 제공됩니다.
실용적 결과¶
우리가 생성하는 JIT 컴파일러는 지금까지 널리 사용되지 않은 몇 가지 기법을 사용하지만, 그렇다고 완전히 새로운 것도 아닙니다. 여기서 우리가 말하고자 하는 요점은 주어진 동적 언어를 얼마나 빠르게 실행할 수 있는지에 대한 이론적 한계를 밀어붙이고 있다는 것이 아닙니다. 우리의 요점은 이것입니다: 우리는 아무리 복잡하거나 널리 쓰이지 않는(예를 들어 대규모 산업계나 학계의 지원이 없는 오픈소스 동적 언어, 또는 내부용 도메인 특화 언어) 언어라 하더라도, 모든 동적 언어에 대해 상당히 우수한 Just-In-Time 컴파일러를 갖추는 것을 실용적으로 만들고 있다는 것입니다. 실용적이라는 것은 다음을 의미합니다:
- 쉬움: 인터프리터를 처음부터 작성하는 것보다 훨씬 더 많은 노력이 필요하지 않습니다.
- 유지보수 가능성: 저희가 생성하는 JIT 컴파일러는 별도의 프로젝트가 아닙니다(별도의 소스 코드를 생성하는 것이 아니라, 생성된 VM에 컴파일되는 일회성 C 코드만 생성합니다). 다시 말해, 고수준 인터프리터가 수정될 때마다 JIT 컴파일러 전체가 처음부터 다시 생성되므로, 언어가 아무리 빠르게 진화하더라도 서로 어긋날 수 없습니다.
- 충분히 빠릅니다: 생성된 JIT 컴파일러에서 꽤 좋은 성능을 얻을 수 있습니다. 물론 그것이 바로 핵심입니다.
속도를 개선하기 위한 대안적 접근 방법¶
|
동적 프로그래밍 언어의 실행 속도를 개선하기 위한 접근법은 다수 존재하지만, 그 대부분은 작은 개선만을 만들어낼 뿐이며 저희의 접근법이 제공하는 유연성과 커스터마이징 가능성을 제공하는 것은 없습니다. 지난 6년간의 조정에도 불구하고 CPython의 속도는 (벤치마크에 따라) 겨우 1.3배에서 1.4배 정도 개선되었을 뿐입니다. 많은 조정이 PyPy에도 적용 가능합니다. 실제로 CPython의 일부 조정은 원래 PyPy를 위한 조정에서 비롯되었습니다.
IronPython은 초기에 언어의 일부 세부 사항을 생략하고 Microsoft가 .NET 플랫폼을 빠르게 만드는 데 투입한 막대한 투자를 활용함으로써 CPython보다 약 1.8배 빠른 속도를 달성했습니다. 하지만 현재의 더 완전한 구현은 CPython과 거의 같은 속도를 갖습니다. 일반적으로, 기존 접근법들은 속도 측면에서 한계에 도달했습니다. 이 맥락에서 자주 언급되는 Microsoft의 Dynamic Language Runtime(DLR)은 본질적으로 IronPython이 개척한 기법들을 공식화하는 API에 불과합니다. 기껏해야, 그것은 또 다른 작은 개선을 가져올 것입니다.
자주 언급되는 또 다른 기법은 언어의 속도를 높이기 위해 타입을 추가하는 것으로, 명시적 선택적 타이핑(explicit optional typing)이거나 소프트 타이핑(soft typing, 즉 추론된 “가능성 있는(likely)” 타입)입니다. Python의 경우, 이 분야의 모든 프로젝트는 언어의 단순화된 부분집합에서 시작했으며, 완전한 언어에 근접할 정도로 규모를 확장한 프로젝트는 없습니다. 이는 상당한 노력이 필요하며 플랫폼 및 언어에 특화될 것입니다. 게다가 유지보수는 골칫거리가 될 것인데, CPython에서 구현하기 사소한 많은 변경 사항이 이전에 공들여 조정한 최적화를 무효화할 가능성이 높다고 생각합니다.
속도의 대폭적인 개선을 위해서는 JIT 기법이 필요합니다. Python의 경우, Psyco는 일반적으로 2배에서 4배의 속도 향상을 제공하며, 알고리즘 예제에서는 최대 100배까지 향상시킵니다. 개발과 유지 관리에 따르는 어려움과 막대한 비용 때문에 막다른 길에 이르렀습니다. 언어 의미론(language semantics)의 부호화가 비교적 빈약하여, Python 동작에 관한 지식을 손으로 직접 부호화하고 최신 상태로 유지해야 합니다. 그나마 Psyco는 지원하지 않는 수많은 Python 구문 중 하나를 만나더라도 CPython으로 대체 실행함으로써 올바르게 동작합니다. PyPy JIT는 메타프로그래밍적이고 특정 언어에 종속되지 않는 Psyco의 대응물로 시작되었습니다.
다른 종류의 선행 기술로는 Jikes와 같은 자기 호스팅 JIT 컴파일러가 있습니다. Jikes는 Java로 작성된 Java용 JIT 컴파일러입니다. 언어 의미론(semantics)의 인코딩이 빈약하며, Python과 유사한 언어의 모든 세부 사항을 JIT 컴파일러에 직접 인코딩하려면 엄청난 양의 작업이 필요할 것입니다. 또한 이식성(portability)이 제한적인데, 이는 Python에서 문제가 되는 부분으로, JIT 컴파일러의 상당 부분을 의도된 저수준 환경과 다른 환경에서 실행하려면 재타겟팅(retargetting)해야 할 가능성이 높습니다.
JVM의 것과 같이 이미 잘 튜닝된 JIT를 단순히 재사용하는 것은 실제로 잘 작동하지 않는데, 이는 구현하고자 하는 언어와 호스트 VM 언어 사이에 개념 불일치가 있기 때문입니다: 전자는 JIT가 상당한 속도 향상을 이끌어낼 수 있는 방식으로 대상 환경에 맞게 컴파일되어야 하는데, 이는 지금까지 Python에서는 본질적으로 실패한 접근법입니다: CPython은 단순한 인터프리터임에도 불구하고, 그 Java 및 .NET 재구현판들은 눈에 띄게 더 빠르지 않습니다.
최근에는 JIT 분야에서 몇몇 더 큰 규모의 프로젝트가 시작되었습니다. 예를 들어, Sun Microsystems는 Ruby의 성능을 개선하기 위해 Java Hotspot JIT를 사용하는 것을 목표로 하는 JRuby에 투자하고 있습니다. 하지만 이는 많은 수작업을 필요로 하며, 하나의 플랫폼에서 하나의 언어에 대해서만 속도 향상을 제공할 것입니다. 동적 언어에서 흔히 발생하는, 지속적인 박싱과 언박싱의 오버헤드를 제거하는 방법과 같이 까다로운 문제들도 있습니다. PyPy와 비교했을 때의 장점은 메타프로그래밍 접근 방식에는 맞지 않는 수작업 최적화를 수행할 수 있다는 점입니다. 하지만 메타프로그래밍은 PyPy JIT를 다양한 언어와 다양한 실행 플랫폼에서 재사용할 수 있게 해줍니다. 두 접근 방식을 결합하는 것도 가능합니다 - 저희 JIT를 사용해 상당한 속도 향상을 얻은 다음, 그 결과를 Java의 Hotspot JIT에 넘겨 추가로 개선할 수 있습니다. 저희 중 한 명은 동적 언어를 더 잘 지원하기 위해 JVM에 추가할 사항을 정의하는 JSR 292 전문가 그룹의 일원이기도 하며, 저희의 JIT 연구에서 얻은 통찰을 제공하여 PyPy에도 도움이 되는 방식으로 기여하고 있습니다.
마지막으로, TraceMonkey를 통해 JavaScript와 같은 동적 언어를 위한 트레이싱 JIT가 이제 등장하고 있습니다. PyPy가 생성한 코드는 트레이싱 JIT의 개념과 매우 유사합니다(다만 직접 작성된 것은 아닙니다).