[Draft] PEP 744 - JIT Compilation
원문 링크: PEP 744 - JIT Compilation
상태: Draft 유형: Informational 작성일: 11-Apr-2024
PEP 744: JIT Compilation
요약 (Abstract)
최근 CPython의 메인 개발 브랜치에 실험적인 JIT(Just-In-Time) 컴파일러가 병합되었습니다. 이 PEP는 JIT 컴파일러의 도입 배경, 설계 결정, 현재 구현 상태, 그리고 CPython의 영구적인 비실험적 기능으로 만들기 위한 향후 계획을 요약합니다. 이 문서는 JIT의 작동 방식에 대한 포괄적인 설명보다는, 선택된 접근 방식의 장단점과 도입 이후 제기된 질문에 답변하는 데 중점을 둡니다.
도입 배경 (Motivation)
기존 CPython은 파이썬 코드를 바이트코드로 컴파일한 후 런타임에 인터프리트(interpret)하는 방식으로 실행했습니다. Python 3.11부터 “specializing adaptive interpreter” (PEP 659)가 도입되어 런타임에 바이트코드 명령어를 타입 특화된 버전으로 재작성하여 상당한 성능 향상을 이루었지만, 개별 바이트코드 명령어의 경계로 인해 최적화 잠재력은 제한적이었습니다. Python 3.12부터는 C-like DSL(Domain-Specific Language)을 사용하여 인터프리터가 생성되며, 이는 유지보수를 용이하게 하고 새로운 실행 방식을 가능하게 합니다.
Python 3.13부터는 마이크로-옵(micro-op) 번역, 최적화 및 실행 메커니즘이 모든 CPython 빌드에 포함되었으나 기본적으로 비활성화되어 있습니다. 인터프리터의 오버헤드가 크기 때문입니다. 이 병목 현상을 극복하기 위한 가장 확실한 전략은 이러한 최적화된 트레이스(traces)를 정적으로 컴파일하는 것입니다. 이는 여러 간접적인 호출 및 인터프리테이션으로 인한 오버헤드를 줄일 수 있는 기회를 제공합니다. 기존 최적화 파이프라인이 런타임 프로파일링 정보를 많이 사용하므로, 코드를 미리 컴파일하는 것보다 실행 “직전”에 최적화된 마이크로-옵을 컴파일하는 JIT 방식이 가장 유망한 접근 방식으로 간주됩니다.
근거 (Rationale)
JIT 컴파일러는 단순히 “더 빠르게” 만드는 마법이 아닙니다. 단일 플랫폼을 위한 최적화 컴파일러를 개발하고 유지보수하는 것은 매우 복잡하고 비용이 많이 드는 작업입니다. LLVM과 같은 기존 컴파일러 프레임워크를 사용하면 작업이 단순해질 수 있지만, 런타임 의존성이 증가하고 JIT 컴파일 오버헤드가 높아지는 단점이 있습니다.
파이썬 코드를 런타임에 성공적으로 컴파일하려면 고품질의 파이썬 특정 최적화와 최적화된 프로그램에 대한 효율적인 머신 코드(machine code)의 빠른 생성이 모두 필요합니다. CPython 코어 개발팀은 전자에 필요한 기술과 경험을 가지고 있으며, “copy-and-patch” 컴파일 방식이 후자에 대한 매력적인 해결책을 제공합니다. Copy-and-patch는 인터프리터의 나머지 부분을 생성하는 데 사용되는 동일한 DSL에서 고품질 템플릿 JIT 컴파일러를 생성할 수 있게 합니다. 이는 CPython 유지보수자가 바이트코드 정의를 편집하는 것만으로도 JIT 백엔드를 모든 JIT 지원 플랫폼에서 한 번에 “무료로” 업데이트할 수 있다는 큰 이점이 있습니다.
JIT 컴파일러는 인터프리터의 나머지 부분과 마찬가지로 빌드 타임(build time)에 생성되며 런타임 의존성이 없습니다. 광범위한 플랫폼을 지원하며 상대적으로 낮은 유지보수 부담을 가집니다. 현재 구현은 약 900줄의 빌드 타임 파이썬 코드와 500줄의 런타임 C 코드로 구성됩니다.
명세 (Specification)
JIT는 현재 기본 빌드 구성의 일부가 아니며, 예측 가능한 미래에도 그럴 가능성이 높습니다 (공식 바이너리에는 포함될 수 있음). JIT가 비실험적 기능이 되기 위한 조건은 다음과 같습니다.
- 적어도 하나의 인기 있는 플랫폼에서 의미 있는 성능 향상(현실적으로 약 5% 수준)을 제공해야 합니다.
- 최소한의 방해로 빌드, 배포 및 배치가 가능해야 합니다.
- Steering Council이 요청 시, 비활성화된 경우보다 활성화된 경우 커뮤니티에 더 많은 가치를 제공한다고 판단해야 합니다 (유지보수 부담, 메모리 사용량 또는 대체 설계의 실현 가능성과 같은 절충안 고려).
이러한 기준은 시작점으로 간주되며 시간이 지남에 따라 확장될 수 있습니다. JIT가 비실험적이 되기 전까지는 프로덕션에서 사용해서는 안 되며, 경고 없이 언제든지 중단되거나 제거될 수 있습니다. JIT가 비실험적이지 않게 되면 --enable-optimizations
또는 --with-lto
와 같은 다른 빌드 옵션과 동일하게 처리됩니다.
지원 (Support)
JIT는 PEP 11의 현재 Tier 1 플랫폼, 대부분의 Tier 2 플랫폼 및 하나의 Tier 3 플랫폼에서 개발되었습니다. CPython의 main
브랜치는 다음 플랫폼에서 JIT에 대한 릴리스 및 디버그 빌드를 CI로 구축하고 테스트합니다.
aarch64-apple-darwin/clang
aarch64-pc-windows/msvc
aarch64-unknown-linux-gnu/clang
aarch64-unknown-linux-gnu/gcc
i686-pc-windows-msvc/msvc
x86_64-apple-darwin/clang
x86_64-pc-windows-msvc/msvc
x86_64-unknown-linux-gnu/clang
x86_64-unknown-linux-gnu/gcc
일부 플랫폼은 JIT 지원을 받지 못할 수도 있습니다 (예: powerpc64le-unknown-linux-gnu/gcc
, wasm32-unknown-wasi/clang
).
JIT 지원이 추가되면 PEP 11에 명시된 대로 신뢰할 수 있는 CI/빌드봇을 가져야 하며, Tier 1 및 Tier 2 플랫폼에서의 JIT 실패는 릴리스를 차단해야 합니다. JIT 지원 제거는 하위 호환성(backwards-incompatible) 변경으로 간주되지 않지만, 합리적인 경우 PEP 387에 설명된 일반적인 Deprecation 프로세스를 따라야 합니다.
하위 호환성 (Backwards Compatibility)
현재 인터프리터와 JIT 백엔드가 동일한 명세(specification)에서 생성되기 때문에 파이썬 코드의 동작은 완전히 변경되지 않아야 합니다. 테스트 중에 발견되고 수정된 관찰 가능한 차이점은 copy-and-patch 단계의 버그보다는 기존 마이크로-옵 번역 및 최적화 단계의 버그인 경향이 있었습니다.
디버깅 (Debugging)
파이썬 코드를 프로파일링하고 디버깅하는 도구는 계속 잘 작동합니다. 여기에는 sys.monitoring
, sys.settrace
, sys.setprofile
과 같은 파이썬 제공 기능을 사용하는 인-프로세스(in-process) 도구와 인터프리터 상태에서 파이썬 프레임을 탐색하는 아웃-오브-프로세스(out-of-process) 도구가 포함됩니다.
그러나 C 코드용 프로파일러 및 디버거는 현재 JIT 프레임을 통해 추적할 수 없는 것으로 보입니다. 리프 프레임(leaf frames)으로 작업하는 것은 가능하지만 (JIT 자체를 디버깅하는 방식), JIT 프레임에 적절한 디버깅 정보가 없기 때문에 유용성이 제한적입니다. 이는 해결해야 할 문제이지만, 현재로서는 우선순위가 높지 않습니다.
보안 영향 (Security Implications)
이 JIT는 다른 JIT와 마찬가지로 런타임에 대량의 실행 가능한 데이터(executable data)를 생성합니다. 이는 악의적인 행위자가 이 데이터의 내용에 영향을 미칠 수 있다면 임의의 코드(arbitrary code)를 실행할 수 있으므로 CPython에 잠재적인 새로운 공격 표면을 도입합니다. 이는 JIT 컴파일러의 잘 알려진 취약점입니다.
이러한 위험을 완화하기 위해 JIT는 모범 사례를 염두에 두고 작성되었습니다. 특히, 해당 데이터는 쓰기 가능한 상태로 유지되는 동안 JIT 컴파일러에 의해 프로그램의 다른 부분에 노출되지 않으며, 데이터가 쓰기 가능하고 실행 가능한 상태가 동시에 되는 지점은 없습니다. 템플릿 기반 JIT의 특성 또한 생성될 수 있는 코드의 종류를 심각하게 제한하여 성공적인 익스플로잇(exploit) 가능성을 더욱 줄입니다. 추가 예방 조치로, 템플릿 자체는 정적 읽기 전용 메모리에 저장됩니다.
Apple Silicon
macOS 릴리스는 Hardened Runtime에 JIT Entitlement를 활성화해야 할 것으로 보입니다. 이는 파이썬 설치를 더 어렵게 만들지는 않지만, 릴리스 관리자가 수행해야 할 추가 단계를 추가할 수 있습니다.
교육 방법 (How to Teach This)
- 파이썬 프로그래머 또는 최종 사용자: 아무것도 변경되지 않습니다. JIT가 실험적 기능인 동안에는 JIT가 활성화된 CPython 인터프리터를 배포받지 않을 것입니다. 비실험적이 되면 약간 더 나은 성능과 약간 더 높은 메모리 사용량을 느낄 수 있지만, 다른 변경 사항은 관찰할 수 없을 것입니다.
- 서드파티 패키지 유지보수자: 아무것도 변경되지 않습니다. API 또는 ABI 변경 사항이 없으며 JIT는 서드파티 코드에 노출되지 않습니다.
- 파이썬 코드 프로파일링 또는 디버깅: 아무것도 변경되지 않습니다. 모든 파이썬 프로파일링 및 트레이싱 기능은 유지됩니다.
- C 코드 프로파일링 또는 디버깅: 현재 JIT 프레임을 통한 추적 기능이 제한적입니다. 전체 C 호출 스택(call stack)을 관찰해야 하는 경우 문제가 발생할 수 있습니다.
- 자체 파이썬 인터프리터 컴파일: JIT를 빌드하고 싶지 않다면 무시해도 됩니다. 그렇지 않으면 호환되는 버전의 LLVM을 설치하고 빌드 스크립트에 적절한 플래그를 전달해야 합니다. 빌드에 최대 1분 더 걸릴 수 있습니다.
- CPython (또는 CPython 포크) 유지보수자:
- 바이트코드 정의 또는 메인 인터프리터 루프 변경: 일반적으로 JIT는 큰 불편을 주지 않을 것입니다. 더 큰 변경 사항(새로운 지역 변수 추가, 오류 처리 변경 등)은 JIT를 생성하는 데 사용되는 C 템플릿에 변경이 필요할 수 있습니다.
- JIT 자체 작업: 파이썬 빌드 스크립트, JIT를 생성하는 데 사용되는 C 템플릿, JIT의 런타임 부분을 구성하는 C 코드를 정기적으로 수정하게 될 것입니다. 어셈블리에 익숙하고 컴파일러 관련 과정을 수강했으며 링커에 대한 블로그 게시물을 읽어본 것이 좋습니다.
- CPython의 다른 부분 유지보수: 아무것도 변경되지 않습니다.
참고 구현 (Reference Implementation)
주요 구현 부분은 다음과 같습니다.
Tools/jit/README.md
: JIT 빌드 지침Python/jit.c
: JIT 컴파일러의 전체 런타임 부분jit_stencils.h
: JIT 생성 템플릿 예시Tools/jit/template.c
: JIT 템플릿을 생성하기 위해 컴파일되는 코드Tools/jit/_targets.py
: 빌드 시 템플릿을 컴파일하고 파싱하는 코드
채택되지 않은 아이디어 (Rejected Ideas)
CPython 외부에서 유지보수 (Maintain it outside of CPython)
JIT를 CPython 외부에서 유지보수하는 것이 가능할 수 있지만, 구현이 인터프리터의 나머지 부분과 너무 밀접하게 연결되어 있어 최신 상태로 유지하는 것이 실제 JIT를 개발하는 것보다 더 어려울 것입니다. 또한, 별도의 JIT 프로젝트 릴리스는 특정 CPython 프리릴리스 및 패치 릴리스와 일치해야 하므로 디버깅 노력을 복잡하게 만들 것입니다. JIT가 이미 상당히 안정적이고 궁극적인 목표는 CPython의 비실험적 부분이 되는 것이므로, main
에 유지하는 것이 최선의 방법으로 보입니다.
기본적으로 활성화 (Turn it on by default)
JIT가 기본적으로 활성화되어야 한다는 제안도 있었지만, 현재 JIT는 기존 특화 인터프리터(specializing interpreter)만큼 빠릅니다. 이는 미약하게 들릴 수 있지만, 상당한 성과이며 이 접근 방식이 추가 개발을 위해 main
에 병합될 만큼 충분히 실행 가능하다고 간주된 주된 이유입니다. JIT가 기존 마이크로-옵 인터프리터에 비해 상당한 이점을 제공하지만, 항상 활성화되었을 때 명확한 이점을 제공하는 것은 아닙니다(특히 메모리 소비 증가 및 추가 빌드 타임 의존성을 고려할 때).
여러 컴파일러 툴체인 지원 (Support multiple compiler toolchains)
Clang은 CPython의 JIT 컴파일에 대한 continuation-passing-style 접근 방식에 필요한 보장된 테일 콜(guaranteed tail calls, musttail
)을 지원하는 유일한 C 컴파일러이기 때문에 특별히 필요합니다. LLVM은 또한 JIT 빌드 프로세스에 필요한 다른 기능(객체 파일 파싱 및 역어셈블리 유틸리티)을 포함하며, 추가 툴체인은 추가적인 테스트 및 유지보수 부담을 초래하므로 현재는 하나의 툴체인(Clang)의 주요 버전만 지원하는 것이 편리합니다.
기본 인터프리터의 바이트코드 컴파일 (Compile the base interpreter’s bytecode)
대부분의 copy-and-patch 선행 기술은 이를 빠른 베이스라인 JIT로 사용하지만, CPython의 JIT는 최적화된 마이크로-옵 트레이스를 컴파일하는 데 이 기술을 사용합니다. 새로운 JIT는 현재 다른 동적 언어 런타임의 “베이스라인” 및 “최적화” 컴파일러 계층 사이에 위치합니다. 이는 CPython이 특화 적응형 인터프리터를 사용하여 런타임 프로파일링 정보를 수집하여 코드의 “핫” 경로를 감지하고 최적화하기 때문입니다. 일반 바이트코드를 copy-and-patch를 사용하여 컴파일하는 것이 가능하지만(초기 프로토타입은 마이크로-옵 인터프리터보다 먼저 이것을 정확히 수행했음), 더 세분화된 마이크로-옵 형식만큼 충분한 최적화 잠재력을 제공하지 않는 것으로 보입니다.
GPU 지원 추가 (Add GPU support)
JIT는 현재 CPU 전용입니다. Numba와 같은 JIT와 달리 NumPy 배열 계산을 CUDA GPU로 오프로드하지 않습니다. 이러한 종류의 특수 작업을 가속화하기 위한 풍부한 도구 생태계가 이미 존재하며, CPython의 JIT는 이를 대체하기 위한 것이 아닙니다. 대신, 더 깊은 GPU 통합의 이점을 얻을 가능성이 적은 범용 파이썬 코드의 성능을 향상시키는 것을 목표로 합니다.
미해결 문제 (Open Issues)
속도 (Speed)
현재 JIT는 대부분의 플랫폼에서 기존 특화 인터프리터만큼 빠릅니다. 상당한 성능 향상을 제공하는 것이 JIT의 모든 동기이므로, 이를 개선하는 것이 현재 최우선 과제입니다. 여러 제안된 개선 사항이 이미 진행 중이며, 이 작업은 GH-115802에서 추적되고 있습니다.
메모리 (Memory)
실행 가능한 머신 코드를 위해 추가 메모리를 할당하므로 JIT는 런타임에 기존 인터프리터보다 더 많은 메모리를 사용합니다. 공식 벤치마크에 따르면 JIT는 현재 기본 인터프리터보다 약 10-20% 더 많은 메모리를 사용합니다. 아직 JIT의 메모리 사용량을 최적화하기 위한 많은 노력이 기울여지지 않았으므로, 이러한 수치는 시간이 지남에 따라 줄어들 최대치를 나타낼 가능성이 높습니다. 이는 중간 우선순위이며 GH-116017에서 추적되고 있습니다.
의존성 (Dependencies)
현재 JIT는 빌드 타임에 LLVM에 의존합니다. LLVM은 개별 마이크로-옵 명령어를 머신 코드 블롭(blobs)으로 컴파일하는 데 사용되며, 이들은 JIT의 템플릿을 형성하기 위해 함께 링크됩니다. JIT는 LLVM에 대한 런타임 의존성이 없으므로 최종 사용자에게 의존성으로 전혀 노출되지 않습니다. JIT를 빌드하면 플랫폼에 따라 빌드 프로세스에 3초에서 60초가 추가됩니다. JIT의 생성된 파일은 Git에 의해 추적되지 않습니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments