PEP 744 – JIT 컴파일
- Author:
- Brandt Bucher <brandt at python.org>, Savannah Ostrowski <savannah at python.org>
- Discussions-To:
- Discourse thread
- Status:
- Draft
- Type:
- Informational
- Created:
- 11-Apr-2024
- Python-Version:
- 3.13
- Post-History:
- 11-Apr-2024
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
올해 초, 실험적인 “just-in-time” 컴파일러가 CPython의 main 개발 브랜치에 병합되었습니다. 최근 CPython 릴리스에 다른 상당한 내부 변경 사항도 포함되었지만, 이 추가 사항은 CPython이 전통적으로 Python 코드를 실행해 온 방식에서 특히 크게 벗어난 것입니다. 따라서 이에 대해 더 폭넓게 논의할 필요가 있습니다.
이 PEP는 이 추가 사항의 이면에 있는 설계 결정, 현재 구현 상태, 그리고 JIT를 CPython의 영구적이고 비실험적인 구성 요소로 만들기 위한 향후 계획을 요약하는 것을 목표로 합니다. 이 문서는 JIT가 어떻게 작동하는지에 대한 포괄적인 개요를 제공하려는 것이 아니라, 선택한 접근 방식의 구체적인 장단점에 초점을 맞추고, JIT가 도입된 이후 제기된 많은 질문에 답하고자 합니다.
새로운 JIT에 대해 더 자세히 알고 싶은 독자는 다음 자료를 참조하시기 바랍니다.
- 2023년 CPython 핵심 개발자 스프린트에서 JIT를 처음 소개한 발표 자료입니다. 여기에는 관련 배경 지식, 사용된 “copy-and-patch” 기법에 대한 간략한 기술적 소개, 그리고 참석한 핵심 개발자들 사이에서 이루어진 해당 설계에 관한 공개 토론이 포함되어 있습니다. 이 발표의 슬라이드는 GitHub에서 확인할 수 있습니다.
- copy-and-patch를 처음 설명한 오픈 액세스 논문입니다.
- 해당 논문의 저자가 Lua용 copy-and-patch JIT 컴파일러의 구현을 자세히 설명한 블로그 게시물입니다. 이는 이 접근 방식을 저수준에서 훌륭하게 설명하지만, 다른 기법도 포함하고 있으며 CPython의 JIT와는 특별히 관련이 없는 구현 결정을 내린다는 점에 유의하십시오.
- 구현 자체입니다.
동기
지금까지 CPython은 항상 Python 코드를 바이트코드로 컴파일하여 실행 시점에 해석하는 방식으로 실행해 왔습니다. 이 바이트코드는 소스 코드의 거의 직접적인 변환입니다. 타입이 지정되지 않았으며 대체로 최적화되지 않았습니다.
Python 3.11 릴리스 이후 CPython은 “전문화 적응형 인터프리터”(PEP 659)를 사용해 왔으며, 이 인터프리터는 실행 중에 이러한 바이트코드 명령을 제자리에서 다시 작성하여 타입별 전문화 버전으로 바꿉니다. 이 새로운 인터프리터는 각 바이트코드 명령의 경계에 의해 최적화 가능성이 제한되는데도 상당한 성능 향상을 제공합니다. 또한 프로그램을 통해 흐르는 타입, 특정 객체의 메모리 배치, 프로그램에서 어떤 경로가 가장 많이 실행되는지와 같은 풍부한 새로운 프로파일링 정보를 수집합니다. 다시 말해, 무엇을 최적화할지와 어떻게 최적화할지에 대한 정보입니다.
Python 3.12 릴리스 이후 CPython은 C와 유사한 도메인별 언어 (DSL)로 이 인터프리터를 생성해 왔습니다. 새로운 적응형 인터프리터의 복잡성을 일부 제어하는 것 외에도, 이 DSL을 사용하면 CPython 유지 관리자들이 인터프리터, 컴파일러, 표준 라이브러리의 여러 부분에서 명령 정의와 동기화해야 하는 번거로운 상용구 코드를 직접 작성하지 않아도 됩니다. 단일 진실 공급원에서 대량의 런타임 인프라를 생성할 수 있는 이 기능은 유지 관리에 편리할 뿐만 아니라, CPython의 실행을 새로운 방식으로 확장할 수 있는 많은 가능성도 열어 줍니다. 예를 들어, 명령어 시퀀스를 더 작은 “마이크로 연산”의 동등한 시퀀스로 변환하기 위한 테이블을 자동으로 생성하고, 이러한 마이크로 연산 시퀀스를 위한 최적화 프로그램을 생성하며, 심지어 이를 실행하기 위한 완전히 별도의 두 번째 인터프리터까지 생성할 수 있습니다.
실제로 Python 3.13 릴리스 주기 초기부터 모든 CPython 빌드에는 바로 이 마이크로 연산 변환, 최적화 및 실행 기능이 포함되어 왔습니다. 그러나 기본적으로 비활성화되어 있습니다. 최적화된 마이크로 연산 트레이스조차 해석하는 오버헤드가 대부분의 코드에 너무 크기 때문입니다. 더 강력한 최적화도 상황을 크게 개선하지는 못할 것입니다. 새로운 최적화로 얻는 효율성 향상이 더 작고 복잡한 마이크로 연산을 해석하는 오버헤드로 상쇄될 가능성이 높기 때문입니다.
이 새로운 병목을 극복하는 가장 분명한 전략은 이러한 최적화된 트레이스를 정적으로 컴파일하는 것입니다. 이렇게 하면 해석으로 도입되는 여러 간접 참조 및 오버헤드를 피할 수 있습니다. 특히 이를 통해 마이크로 연산 사이의 디스패치 오버헤드(일반 인터프리터를 핫 코드의 직선형 시퀀스로 대체), 개별 마이크로 연산의 명령 디코딩 오버헤드(인자, 상수 및 캐시된 값의 값이나 주소를 기계 명령에 직접 “구워 넣기”), 그리고 메모리 트래픽(힙에 할당된 Python 프레임에서 데이터를 가져와 물리적 하드웨어 레지스터로 옮기기)을 제거할 수 있습니다.
이 데이터의 상당 부분은 동일한 프로그램 실행 간에도 달라지며 기존 최적화 파이프라인은 런타임 프로파일링 정보를 많이 활용하므로, 이러한 트레이스를 미리 컴파일하는 것은 별로 의미가 없으며 이미 구현된 기존 사양 및 마이크로 연산 트레이싱 인프라를 대대적으로 재설계해야 합니다. 다른 많은 동적 언어(and even Python itself에서도 입증되었듯이), 가장 유망한 접근 방식은 최적화된 마이크로 연산을 실행 시점에 맞춰 컴파일하는 것입니다.
근거
JIT 컴파일러는 평판과 달리 마법 같은 “더 빠르게 실행하는” 기계가 아닙니다. 단일 플랫폼을 위한 최적화 컴파일러만 개발하고 유지 관리하는 것도, CPython이 지원하는 가장 인기 있는 모든 플랫폼을 대상으로 하는 것은 말할 것도 없이, 믿을 수 없을 만큼 복잡하고 비용이 많이 드는 작업입니다. LLVM과 같은 기존 컴파일러 프레임워크를 사용하면 이 작업을 더 간단하게 만들 수 있지만, 그 대가로 무거운 런타임 의존성과 상당히 높은 JIT 컴파일 오버헤드를 도입해야 합니다.
런타임에 Python 코드를 성공적으로 컴파일하려면 실행 중인 코드에 대한 고품질 Python 전용 최적화뿐만 아니라 또한 최적화된 프로그램을 위한 효율적인 머신 코드를 신속하게 생성해야 한다는 점은 분명합니다. Python 핵심 개발 팀은 전자(인터프리터와 긴밀하게 결합된 미들엔드)에 필요한 기술과 경험을 갖추고 있으며, copy-and-patch 컴파일은 후자에 대한 매력적인 해결책을 제공합니다.
간단히 말해, copy-and-patch를 사용하면 인터프리터의 나머지 부분을 생성하는 데 사용되는 것과 동일한 DSL로 고품질 템플릿 JIT 컴파일러를 생성할 수 있습니다. CPython처럼 널리 사용되며 자원봉사자 중심으로 운영되는 프로젝트에서는 이 이점을 아무리 강조해도 지나치지 않습니다. CPython의 유지 관리자들은 바이트코드 정의를 단순히 편집하기만 해도 모든 JIT 지원 플랫폼에서 JIT 백엔드도 한 번에 “무료로” 업데이트할 수 있습니다. 이는 명령어를 추가하거나 수정하거나 제거하는 경우에도 동일하게 적용됩니다.
인터프리터의 나머지 부분과 마찬가지로 JIT 컴파일러는 빌드 시점에 생성되며 런타임 의존성이 없습니다. 광범위한 플랫폼을 지원하며(아래의 Support 섹션 참조), 유지 관리 부담도 비교적 낮습니다. 전체적으로 현재 구현은 빌드 시점 Python 코드 약 900줄과 런타임 C 코드 500줄로 구성되어 있습니다.
사양
JIT는 현재 기본 빌드 구성에 포함되어 있지 않으며, 가까운 미래에도 그 상태가 유지될 가능성이 높습니다(다만 공식 바이너리에는 포함될 수 있습니다). 그렇지만 다음 조건을 모두 충족하면 JIT는 실험적 기능이 아니게 됩니다.
- 적어도 하나의 인기 플랫폼에서 의미 있는 성능 향상을 제공합니다(현실적으로는 약 5% 수준입니다).
- 최소한의 중단으로 빌드하고, 배포하고, 적용할 수 있습니다.
- 운영 위원회가 요청을 받아, 유지 관리 부담, 메모리 사용량 또는 대체 설계의 실행 가능성 등의 상충 요소를 고려했을 때 JIT를 비활성화했을 때보다 활성화했을 때 커뮤니티에 더 큰 가치를 제공한다고 판단합니다.
이러한 기준은 출발점으로 간주해야 하며 시간이 지나면서 확장될 수 있습니다. 예를 들어 이 PEP에 대한 논의를 통해 추가 요구 사항(여러 명의 확정된 유지 관리자, 보안 감사, devguide의 문서화, 프로세스 외부 디버깅 지원 또는 JIT를 비활성화하는 런타임 옵션 등)을 이 목록에 추가해야 한다는 사실이 드러날 수 있습니다.
JIT가 실험적 기능이 아닌 상태가 될 때까지는 프로덕션에서 사용해서는 안 되며, 경고 없이 언제든지 손상되거나 제거될 수 있습니다.
JIT가 더 이상 실험적 기능이 아니게 되면 --enable-optimizations 또는 --with-lto와 같은 다른 빌드 옵션과 거의 동일한 방식으로 취급해야 합니다. 일부 플랫폼에서는 권장 옵션(또는 기본 옵션)이 될 수 있으며, 릴리스 관리자는 공식 릴리스에서 이를 활성화할 수도 있습니다.
지원
JIT는 PEP 11의 현재 1단계 플랫폼 전체, 2단계 플랫폼 대부분 및 3단계 플랫폼 하나를 대상으로 개발되었습니다. 구체적으로 CPython의 main 브랜치에는 릴리스 빌드와 디버그 빌드 모두에서 JIT를 빌드하고 테스트하는 CI가 다음 환경에 구성되어 있습니다.
aarch64-apple-darwin/clangaarch64-pc-windows/msvc[1]aarch64-unknown-linux-gnu/clang[2]aarch64-unknown-linux-gnu/gcc[2]i686-pc-windows-msvc/msvcx86_64-apple-darwin/clangx86_64-pc-windows-msvc/msvcx86_64-unknown-linux-gnu/clangx86_64-unknown-linux-gnu/gcc
일부 플랫폼, 심지어 향후 1등급 플랫폼도 JIT 지원을 영원히 얻지 못할 수 있다는 점에 유의할 필요가 있습니다. 이는 불충분한 LLVM 지원(powerpc64le-unknown-linux-gnu/gcc), 플랫폼 자체의 한계(wasm32-unknown-wasi/clang), 또는 개발자의 관심 부족(x86_64-unknown-freebsd/clang)을 비롯한 다양한 이유 때문일 수 있습니다.
플랫폼에 대한 JIT 지원이 추가되면(즉, 사용자에게 경고를 표시하지 않고 JIT가 성공적으로 빌드되면), PEP 11에서 규정하는 것과 거의 동일한 방식으로 처리해야 합니다. 안정적인 CI/빌드봇을 갖추어야 하며, 1등급 및 2등급 플랫폼에서의 JIT 실패는 릴리스를 차단해야 합니다. JIT 지원을 명시하도록 PEP 11을 업데이트할 필요는 없지만, 그렇게 하는 것이 도움이 될 수도 있습니다. 그렇지 않으면 지원되는 플랫폼 목록을 JIT의 README에 유지해야 합니다.
JIT 없이도 CPython을 항상 빌드할 수 있어야 하므로, 플랫폼에 대한 JIT 지원 제거는 not하위 호환성을 깨뜨리는 변경으로 간주해야 합니다. 그러나 그렇게 하는 것이 합리적이라면 PEP 387에 설명된 일반적인 사용 중단 절차를 따라야 합니다.
JIT의 빌드 시 의존성은 합리적인 범위 내에서 릴리스 간에 변경될 수 있습니다.
하위 호환성
현재 인터프리터와 JIT 백엔드가 모두 동일한 사양에서 생성된다는 사실로 인해 Python 코드의 동작은 완전히 변경되지 않아야 합니다. 실제로 테스트 중 발견되어 수정된 관찰 가능한 차이는 copy-and-patch 단계의 버그라기보다는 기존 마이크로 연산 변환 및 최적화 단계의 버그인 경우가 많았습니다.
디버깅
Python 코드를 프로파일링하고 디버깅하는 도구는 계속해서 정상적으로 작동합니다. 여기에는 Python이 제공하는 기능(예: sys.monitoring, sys.settrace, 또는 sys.setprofile)을 사용하는 프로세스 내부 도구와, 인터프리터 상태에서 Python 프레임을 순회하는 프로세스 외부 도구가 모두 포함됩니다.
그러나 현재 C 코드에 대한 프로파일러와 디버거는 JIT 프레임을 거슬러 추적할 수 없는 것으로 보입니다. 리프 프레임을 대상으로 작업하는 것은 가능하지만(이 방식으로 JIT 자체를 디버깅합니다), JIT 프레임에 대한 적절한 디버깅 정보가 없기 때문에 그 유용성은 제한적입니다.
JIT가 생성하는 코드 템플릿은 Clang으로 컴파일되므로, 컴파일러 플래그를 수정하여 프레임 포인터를 더욱 신중하게 사용하면 JIT 프레임을 거슬러 추적할 수 있게 하는 것이 가능할수도 있습니다. Clang이 생성한 디버깅 정보를 수집하여 출력하는 것도 가능할 수 있습니다. 이 두 가지 아이디어 모두 매우 깊이 있게 탐구되지는 않았습니다.
이는 수정되어야 하는 문제이지만, 현재 이를 수정하는 것은 특별히 우선순위가 높지 않습니다. 이는 JIT 유지 관리자들과 협력하면서 이러한 도구의 내부 작동 방식에 대한 경험이 많지 않은, 해당 분야의 전문 지식이 더 많은 누군가가 탐구하는 것이 가장 적절한 문제일 것입니다.
보안 관련 사항
이 JIT는 다른 모든 JIT와 마찬가지로 런타임에 대량의 실행 가능 데이터를 생성합니다. 따라서 이 데이터의 내용을 조작할 수 있는 악의적인 행위자는 임의의 코드를 실행할 수도 있으므로, 이는 CPython에 잠재적으로 새로운 공격 표면을 도입합니다. 이는 JIT 컴파일러의 잘 알려진 취약점입니다.
이러한 위험을 완화하기 위해 JIT는 모범 사례를 염두에 두고 작성되었습니다. 특히 문제의 데이터는 쓰기 가능한 상태로 유지되는 동안 JIT 컴파일러에 의해 프로그램의 다른 부분에 노출되지 않으며, 어떠한 시점에도 데이터가 writable and executable 상태이면서
템플릿 기반 JIT의 특성은 생성할 수 있는 코드의 종류를 심각하게 제한하여, 성공적인 익스플로잇 가능성을 더욱 낮춥니다. 추가적인 예방 조치로 템플릿 자체는 정적 읽기 전용 메모리에 저장됩니다.
그러나 특히 아직 초기 단계인 현재, JIT에 가능한 취약점이 전혀 존재하지 않는다고 가정하는 것은 순진한 생각입니다. 작성자는 보안 전문가가 아니지만, 보안 문제가 발생하는 대로 이를 분류하고 수정하기 위해 Python 보안 대응 팀에 참여하거나 긴밀히 협력할 수 있습니다.
Apple Silicon
실제로 macOS 릴리스에 서명하고 패키징하지 않고서는 테스트하기 어렵지만, macOS 릴리스는 Hardened Runtime에 대한 JIT Entitlement를 활성화해야 하는 것으로 보입니다.
이로 인해 Python을 설치하는 일이 더 어려워지지는 않지만, 릴리스 관리자가 수행해야 할 단계가 추가될 수 있습니다.
이것을 가르치는 방법
자신을 가장 잘 설명하는 섹션을 선택하십시오:
- Python 프로그래머 또는 최종 사용자인 경우…
- …아무것도 달라지지 않습니다. 실험적 기능인 동안에는 누구도 JIT가 활성화된 CPython 인터프리터를 여러분에게 배포해서는 안 됩니다. 실험적 기능이 아니게 되면 성능이 약간 향상되고 메모리 사용량이 약간 증가한 것을 알아차리게 될 것입니다. 그 밖의 변화는 관찰할 수 없어야 합니다.
- 서드파티 패키지를 유지 관리하는 경우…
- …아무것도 달라지지 않습니다. API나 ABI 변경은 없으며, JIT는 서드파티 코드에 노출되지 않습니다. CI 매트릭스를 변경할 필요가 없으며, JIT가 활성화되었을 때 패키지가 작동하는 방식의 차이도 관찰할 수 없어야 합니다.
- Python 코드를 프로파일링하거나 디버깅하는 경우…
- …아무것도 달라지지 않습니다. 모든 Python 프로파일링 및 트레이싱 기능은 그대로 유지됩니다.
- C 코드를 프로파일링하거나 디버깅하는 경우…
- …현재 JIT 프레임을 통과하여 추적하는 기능은 제한적입니다. 단순히 “리프” 프레임만이 아니라 전체 C 호출 스택을 관찰해야 하는 경우 문제가 발생할 수 있습니다. 자세한 내용은 위의 Debugging 섹션을 참조하십시오.
- 자체 Python 인터프리터를 컴파일하는 경우….
- …JIT를 빌드하지 않으려면 간단히 무시할 수 있습니다. 그렇지 않으면 호환되는 LLVM 버전을 설치하고 빌드 스크립트에 적절한 플래그를 전달해야 합니다. 빌드 시간이 최대 1분 더 길어질 수 있습니다. JIT가 아직 실험 단계인 동안에는 최종 사용자에게 배포하거나 프로덕션에서 사용해서는 안 된다는 점에 유의하십시오.
- CPython(또는 CPython의 포크)의 유지 관리자인 경우…
- …바이트코드 정의 또는 주 인터프리터 루프를 변경하는 경우…
- …일반적으로 JIT는 (하려는 작업에 따라 다르지만) 사용자에게 큰 불편을 주지는 않아야 합니다. 마이크로 연산 인터프리터는 사라지지 않으며, 현재 주 바이트코드 인터프리터가 제공하는 것과 유사한 디버깅 경험을 계속 제공합니다. 인터프리터에 대한 더 큰 변경 사항(예: 새로운 지역 변수 추가, 오류 처리 및 디옵티마이제이션 로직 변경, 마이크로 연산 형식 변경)에는 JIT를 생성하는 데 사용되는 C 템플릿의 변경이 필요할 가능성이 보통 정도로 있습니다. 이 템플릿은 주 인터프리터 루프를 모방하도록 설계되었습니다. 때로는 운이 나빠 JIT 코드 생성을 중단할 수도 있으며, 이 경우 Python 빌드 스크립트를 직접 수정하거나 해당 스크립트에 더 익숙한 사람에게 도움을 요청해야 합니다(아래 참조).
- …그리고 JIT 자체를 작업합니다…
- …자신이 무엇에 뛰어드는지 이미 어느 정도 잘 알고 계시기를 바랍니다. Python 빌드 스크립트, JIT를 생성하는 데 사용되는 C 템플릿, 그리고 실제로 JIT의 런타임 부분을 구성하는 C 코드를 정기적으로 수정하게 됩니다. 또한 온갖 종류의 충돌을 처리하고, 디버거에서 머신 코드를 한 단계씩 실행하며, COFF/ELF/Mach-O 덤프를 들여다보고, 매우 다양한 플랫폼에서 개발하며, 일반적으로 CI가 해당 PR에서 바이트코드를 변경한 사람들의 PR에 실패하기 시작할 때 연락 창구 역할을 하게 됩니다(위 참조). 이상적으로는 최소한 어셈블리에 익숙해야 하고, 이름에 “컴파일러”가 들어간 강의를 두 개쯤 수강했으며, 링커에 관한 블로그 글도 한두 편 읽었어야 합니다.
- …그리고 CPython의 다른 부분을 유지 관리합니다…
- …아무것도 달라지지 않습니다. JIT 빌드로 로컬에서 개발할 필요는 없습니다. 그렇게 하기로 선택하는 경우(예를 들어 JIT 문제를 재현하고 분류하는 데 도움을 주기 위해), 관련 파일이 수정될 때마다 빌드 시간이 최대 1분 더 길어질 수 있습니다.
- …바이트코드 정의 또는 주 인터프리터 루프를 변경하는 경우…
참조 구현
구현의 주요 부분은 다음과 같습니다.
Tools/jit/README.md: JIT를 빌드하는 방법에 대한 지침입니다.Python/jit.c: JIT 컴파일러의 전체 런타임 부분입니다.jit_stencils.h: JIT가 생성한 템플릿의 예입니다.Tools/jit/template.c: JIT의 템플릿을 생성하기 위해 컴파일되는 코드입니다.Tools/jit/_targets.py: 빌드 시 템플릿을 컴파일하고 구문 분석하는 코드입니다.
거부된 아이디어
CPython 외부에서 유지 관리하기
JIT를 CPython 외부에서 유지 관리하는 것이 아마도 가능하겠지만, JIT의 구현은 인터프리터의 나머지 부분과 충분히 긴밀하게 연결되어 있으므로 최신 상태로 유지하는 일이 JIT 자체를 실제로 개발하는 것보다 더 어려울 가능성이 큽니다. 또한 기존 마이크로 연산 정의와 최적화 작업을 하는 기여자는 JIT에서 변경 사항의 효과를 측정하려면 두 개의 별도 프로젝트를 수정하고 빌드해야 합니다(반면 현재는 제안된 모든 변경 사항에 대해 이를 자동으로 수행하는 인프라가 존재합니다).
별도의 “JIT” 프로젝트 릴리스는 정확히 어떤 변경 사항이 포함되었는지에 따라 특정 CPython 프리릴리스 및 패치 릴리스와 대응해야 할 가능성도 큽니다. 릴리스 사이의 개별 CPython 커밋에는 대응하는 JIT 릴리스가 아예 없을 가능성이 높으며, 이는 업스트림에서 문제를 일으킨 변경 사항을 찾기 위한 이분 탐색과 같은 디버깅 작업을 더욱 복잡하게 만듭니다.
JIT는 이미 상당히 안정적이고, 궁극적인 목표는 JIT를 CPython의 비실험적 구성 요소로 만드는 것이므로, JIT를 main에 유지하는 것이 앞으로 나아갈 최선의 경로인 듯합니다. 그렇다고 해도 관련 코드는 JIT가 목표를 달성하지 못할 경우 쉽게 “삭제”할 수 있도록 구성되어 있습니다.
기본적으로 활성화하기
반면 일부 사람들은 현재 형태의 JIT를 기본적으로 활성화해야 한다고 제안했습니다.
다시 말하지만, JIT는 마법의 “더 빠르게 실행하는” 기계가 아니라는 점을 기억하는 것이 중요합니다. 현재 JIT는 기존의 특수화 인터프리터만큼 빠릅니다. 이는 기대에 못 미치는 것처럼 들릴 수 있지만 실제로는 상당히 중요한 성과이며, 이 접근 방식이 추가 개발을 위해 main에 병합할 만큼 실현 가능하다고 판단된 주된 이유입니다.
JIT는 기존 마이크로 연산 인터프리터보다 상당한 성능 향상을 제공하지만, 항상 활성화했을 때는 아직 명확한 이점이라고 보기 어렵습니다(특히 증가한 메모리 사용량과 추가 빌드 시점 의존성을 고려하면). 이것이 이 PEP의 목적입니다. 즉, “스위치를 켜기” 위해 충족해야 하는 객관적 기준에 대한 기대치를 명확히 하는 것입니다.
적어도 현재로서는 main에 포함하되 기본적으로 비활성화하는 것이, 항상 활성화하는 것과 전혀 사용할 수 없게 하는 것 사이의 좋은 절충안인 것 같습니다.
여러 컴파일러 도구 모음 지원
Clang이 특별히 필요한 이유는 보장된 테일 호출을 지원하는 유일한 C 컴파일러이기 때문이며, 이러한 호출은 CPython의 연속 전달 방식 JIT 컴파일 접근 방식에 필요합니다. 이것이 없으면 템플릿 간 테일 재귀 호출로 인해 C 스택이 제한 없이 증가하여 결국 오버플로가 발생할 수 있습니다.
LLVM에는 JIT 빌드 프로세스에 필요한 다른 기능(구체적으로 객체 파일 구문 분석 및 디스어셈블리를 위한 유틸리티)도 포함되어 있으며, 추가 도구 모음은 테스트 및 유지 관리 부담을 늘리므로, 현재로서는 하나의 도구 모음의 하나의 메이저 버전만 지원하는 것이 편리합니다.
기본 인터프리터의 바이트코드 컴파일
copy-and-patch에 관한 기존 구현 대부분은 이를 빠른 베이스라인 JIT로 사용하는 반면, CPython의 JIT는 이 기법을 사용하여 최적화된 마이크로 연산 추적을 컴파일합니다.
실제로 새로운 JIT는 현재 다른 동적 언어 런타임의 “베이스라인” 컴파일러 계층과 “최적화” 컴파일러 계층 사이 어딘가에 위치합니다. 이는 CPython이 전문화 적응형 인터프리터를 사용하여 런타임 프로파일링 정보를 수집하고, 이를 통해 코드 내 “핫” 경로를 감지하고 최적화하기 때문입니다. 이 단계는 자기 수정 코드를 사용하여 수행되며, 이는 JIT 컴파일러로 구현하기가 훨씬 더 어려운 기법입니다.
copy-and-patch를 사용하여 일반 바이트코드를 컴파일하는 것은 가능하지만(실제로 초기 프로토타입은 마이크로 연산 인터프리터보다 먼저 개발되었으며 정확히 이 작업을 수행했습니다), 더 세분화된 마이크로 연산 형식만큼 충분한 최적화 잠재력을 제공하지는 않는 것 같습니다.
GPU 지원 추가
JIT는 현재 CPU만 지원합니다. 예를 들어 Numba와 같은 JIT가 수행하는 것처럼 NumPy 배열 계산을 CUDA GPU로 오프로딩하지는 않습니다.
이러한 종류의 전문화된 작업을 가속하기 위한 풍부한 도구 생태계가 이미 존재하며, CPython의 JIT는 이를 대체하려는 목적이 아닙니다. 대신 일반적인 Python 코드의 성능을 개선하는 것이 목적이며, 이러한 코드는 더 심층적인 GPU 통합의 혜택을 받을 가능성이 낮습니다.
미해결 문제
속도
현재 JIT는 대부분의 플랫폼에서 기존 전문화 인터프리터와 거의 비슷한 속도입니다. JIT를 도입하는 근본적인 동기가 상당한 성능 향상을 제공하는 것이므로, 현 시점에서 이를 개선하는 것은 당연히 최우선 과제입니다. 여러 제안된 개선 사항이 이미 진행 중이며, 이 작업은 GH-115802에서 추적되고 있습니다.
메모리
실행 가능한 기계어 코드를 위한 추가 메모리를 할당하므로, JIT는 런타임에 기존 인터프리터보다 더 많은 메모리를 사용합니다. 공식 벤치마크에 따르면 JIT는 현재 기본 인터프리터보다 약 10~20% 더 많은 메모리를 사용합니다. 이 범위의 상한은 더 큰 페이지 크기를 가지는 aarch64-apple-darwin 때문이며, 따라서 최소 할당 단위도 더 큽니다.
그러나 벤치마크 자체의 메모리 사용량 기준치가 실제로 그다지 높지 않으므로, 이 수치는 액면 그대로 받아들여서는 안 됩니다. 코드 대 데이터 비율이 더 높기 때문에 JIT의 메모리 오버헤드가 일반적인 워크로드에서보다 더 두드러지며, 일반적인 워크로드에서는 메모리 압박이 실제 문제가 될 가능성이 더 높습니다.
아직 JIT의 메모리 사용량을 최적화하는 데 많은 노력이 투입되지는 않았으므로, 이 수치는 시간이 지나면서 감소할 최대치일 가능성이 높습니다. 이를 개선하는 것은 중간 우선순위 과제이며, GH-116017에서 추적되고 있습니다. 향후 메모리 소비를 제한하기 위한 구성 가능한 매개변수를 공개하는 방안을 고려할 수 있지만, JIT가 비실험적이라고 간주될 요건을 충족하기 전까지는 공식 API를 공개하지 않을 것입니다.
JIT의 이전 버전에는 더 복잡한 메모리 할당 방식이 사용되었으며, 이 방식은 방출된 코드의 크기와 배치에 여러 가지 깨지기 쉬운 제약을 부과하고 Python 실행 파일의 메모리 사용량을 크게 늘렸습니다. 현재 설계에는 이러한 문제가 더 이상 존재하지 않습니다.
의존성
이 글을 작성하는 시점에 JIT는 빌드 시 LLVM에 의존합니다. LLVM은 개별 마이크로 연산 명령을 기계어 코드 블롭으로 컴파일하는 데 사용되며, 이러한 블롭은 함께 링크되어 JIT의 템플릿을 구성합니다. 이러한 템플릿은 CPython 자체를 빌드하는 데 사용됩니다. JIT는 LLVM에 대한 런타임 의존성이 없으므로 최종 사용자에게 의존성으로 전혀 노출되지 않습니다.
JIT를 빌드하면 플랫폼에 따라 빌드 프로세스에 3초에서 60초가 추가됩니다. 생성된 파일이 최신 상태가 아닐 때만 다시 빌드되므로, 메인 인터프리터 루프를 적극적으로 개발하는 사람들만 이를 자주 다시 빌드하게 됩니다.
CPython의 다른 많은 생성 파일과 달리 JIT의 생성 파일은 Git에서 추적하지 않습니다. 이는 해당 파일에 호스트 플랫폼뿐만 아니라 그 플랫폼의 현재 빌드 구성에도 특화된 컴파일된 바이너리 코드 템플릿이 포함되어 있기 때문입니다. 따라서 생성된 코드가 변경되는 각 커밋에 대해 수십 개의 대형 바이너리 파일을 빌드하고 호스팅하려면 상당한 엔지니어링 작업이 필요합니다. 어쩌면 가능할 수도 있지만, CPython을 빌드하는 대부분의 사람에게 필요한 도구를 설치하는 일이 지나치게 어렵지 않고 빌드 단계에도 특별히 많은 시간이 걸리지 않으므로 이는 우선순위가 아닙니다.
일부 사람들은 여전히 이 가능성에 관심을 가지고 있으므로, 논의가 GH-115869에서 추적되고 있습니다.
각주
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.