[Final] PEP 659 - Specializing Adaptive Interpreter
원문 링크: PEP 659 - Specializing Adaptive Interpreter
상태: Final 유형: Informational 작성일: 13-Apr-2021
PEP 659: 특수화 적응형 인터프리터 (Specializing Adaptive Interpreter)
요약 (Abstract)
동적 언어용 가상 머신(Virtual Machine)이 좋은 성능을 내기 위해서는 실행되는 프로그램의 타입(type)과 값(value)에 맞춰 코드를 특수화(specialize)해야 합니다. 이러한 특수화는 종종 JIT(Just-In-Time) 컴파일러와 연관되지만, 기계어 코드 생성 없이도 이점을 제공합니다.
특수화 적응형 인터프리터(Specializing Adaptive Interpreter)는 현재 작동 중인 타입이나 값에 대해 추측성으로 특수화를 수행하고, 이러한 타입과 값의 변화에 적응하는 인터프리터입니다. 특수화는 성능 향상을 제공하며, 적응(adaptation)은 프로그램 사용 패턴이 변경될 때 인터프리터가 빠르게 조정되어, 잘못된 특수화로 인한 추가 작업량을 제한할 수 있도록 합니다.
이 PEP는 코드를 적극적으로 특수화하지만 매우 작은 영역에 대해 수행하고, 잘못된 특수화에 빠르고 저렴하게 적응할 수 있는 특수화 적응형 인터프리터 사용을 제안합니다. CPython에 특수화 적응형 인터프리터를 추가하면 상당한 성능 향상을 가져올 것입니다. 벤치마크와 아직 진행되지 않은 작업에 따라 달라지기 때문에 의미 있는 수치를 제시하기는 어렵지만, 광범위한 실험 결과 최대 50%의 속도 향상을 시사합니다. 25%의 속도 향상만 있더라도 가치 있는 개선이 될 것입니다.
동기 (Motivation)
Python은 느리다고 널리 알려져 있습니다. Python이 C, Fortran 또는 Java와 같은 저수준 언어의 성능에는 도달하지 못하겠지만, Javascript의 V8이나 Lua의 luajit처럼 스크립트 언어의 빠른 구현체들과 경쟁할 수 있기를 바랍니다. 특히, PyPy나 다른 대체 가상 머신을 사용할 수 없는 사용자들을 포함한 모든 Python 사용자에게 혜택을 주기 위해 CPython에서 이러한 성능 목표를 달성하고자 합니다.
이러한 성능 목표를 달성하는 데는 아직 많은 노력이 필요하지만, 인터프리터 속도를 높임으로써 목표를 향한 중요한 발걸음을 내디딜 수 있습니다. 학술 연구와 실제 구현 모두 빠른 인터프리터가 빠른 가상 머신의 핵심 부분임을 보여주었습니다.
가상 머신의 일반적인 최적화는 비용이 많이 들기 때문에, 최적화 비용이 정당하다는 확신을 얻으려면 긴 “웜업(warm up)” 시간이 필요합니다. 눈에 띄는 웜업 시간 없이 빠르게 속도 향상을 얻으려면, VM은 함수가 몇 번만 실행된 후에도 특수화가 정당하다고 추측해야 합니다. 이를 효과적으로 수행하려면 인터프리터는 지속적으로 그리고 매우 저렴하게 최적화 및 비최적화(de-optimize)를 할 수 있어야 합니다.
개별 가상 머신 명령어 단위로 적응적(adaptive)이고 추측성(speculative) 특수화를 사용함으로써, 더 빠른 인터프리터를 얻을 수 있으며, 이는 미래에 더 정교한 최적화를 위한 프로파일링 정보도 생성합니다.
근거 (Rationale)
동적 언어용 가상 머신의 속도를 높이는 실용적인 방법은 많습니다. 그러나 특수화는 그 자체로도 중요하고 다른 최적화를 가능하게 하는 핵심 요소이므로, CPython의 성능을 개선하고자 한다면 특수화에 노력을 집중하는 것이 합리적입니다.
특수화는 일반적으로 JIT 컴파일러의 맥락에서 이루어지지만, 연구에 따르면 인터프리터 내의 특수화도 성능을 크게 향상시킬 수 있으며, 심지어 단순한 컴파일러보다 우수할 수도 있습니다.
학술 문헌에서는 이를 수행하는 여러 방법이 제안되었지만, 대부분 단일 바이트코드(bytecode)보다 큰 영역을 최적화하려고 시도합니다. 단일 명령어보다 큰 영역을 사용하면 영역 중간에 비최적화를 처리하는 코드가 필요합니다. 개별 바이트코드 수준에서의 특수화는 영역 중간에 비최적화가 발생할 수 없으므로 비최적화를 사소하게 만듭니다.
개별 바이트코드를 추측성으로 특수화함으로써, 가장 지역적이고 구현하기 쉬운 비최적화 외에 다른 것은 필요 없이 상당한 성능 향상을 얻을 수 있습니다.
이 PEP에 가장 가까운 접근 방식은 “Inline Caching meets Quickening” 입니다. 이 PEP는 인라인 캐싱(inline caching)의 장점을 가지면서도 빠르게 비최적화할 수 있는 기능을 추가하여, 특수화가 실패하거나 안정적이지 않은 경우에도 성능을 더욱 견고하게 만듭니다.
성능 (Performance)
특수화로 인한 속도 향상은 다른 최적화에 따라 달라지기 때문에 측정하기 어렵습니다. 속도 향상은 10%~60% 범위인 것으로 보입니다.
속도 향상의 대부분은 특수화에서 직접 발생합니다. 가장 큰 기여 요인은 속성 조회(attribute lookup), 전역 변수(global variables), 그리고 호출(calls)의 속도 향상입니다. 작지만 유용한 부분은 슈퍼 명령어(super-instructions)와 quickening으로 가능해진 다른 최적화와 같은 개선된 디스패치(dispatch)에서 나옵니다.
구현 (Implementation)
개요 (Overview)
특수화의 이점을 얻을 수 있는 모든 명령어는 해당 명령어의 “적응형(adaptive)” 형태로 대체됩니다. 실행될 때, 적응형 명령어는 자신이 보는 타입과 값에 반응하여 스스로를 특수화합니다. 이 과정을 “Quickening“이라고 합니다.
코드 객체(code object)의 한 명령어가 충분히 여러 번 실행되면, 해당 명령어는 해당 작업에 대해 더 빠르게 실행될 것으로 예상되는 새 명령어로 대체되어 “특수화“됩니다.
Quickening
Quickening은 느린 명령어를 더 빠른 변형으로 대체하는 과정입니다.
Quickened 코드는 변경 불가능한 바이트코드(immutable bytecode)에 비해 여러 장점이 있습니다.
- 런타임에 변경될 수 있습니다.
- 여러 줄에 걸쳐 여러 피연산자(operand)를 사용하는 슈퍼 명령어(super-instructions)를 사용할 수 있습니다.
- 트레이싱(tracing)을 처리할 필요가 없습니다. 트레이싱의 경우 원본 바이트코드로 폴백(fallback)할 수 있습니다.
트레이싱이 지원될 수 있도록, quickened 명령어 형식은 변경 불가능한, 사용자에게 보이는 바이트코드 형식과 일치해야 합니다. 즉, 8비트 opcode 뒤에 8비트 피연산자가 오는 16비트 명령어 형식입니다.
적응형 명령어 (Adaptive instructions)
특수화의 이점을 얻을 수 있는 각 명령어는 quickening 동안 적응형 버전으로 대체됩니다. 예를 들어, LOAD_ATTR
명령어는 LOAD_ATTR_ADAPTIVE
로 대체됩니다.
각 적응형 명령어는 주기적으로 스스로를 특수화하려고 시도합니다.
특수화 (Specialization)
CPython 바이트코드는 많은 고수준(high-level) 작업을 나타내는 명령어를 포함하고 있으며, 이들은 특수화의 이점을 얻을 수 있습니다. 예시로는 CALL
, LOAD_ATTR
, LOAD_GLOBAL
, BINARY_ADD
등이 있습니다.
이러한 각 명령어에 대해 특수화된 명령어의 “패밀리(family)”를 도입함으로써 효과적인 특수화가 가능해집니다. 각 새로운 명령어는 단일 작업에 특수화되기 때문입니다. 각 패밀리에는 카운터를 유지하고 해당 카운터가 0에 도달하면 스스로를 특수화하려는 “적응형(adaptive)” 명령어가 포함됩니다.
각 패밀리에는 입력이 예상대로인 경우 일반 작업과 동일한 작업을 훨씬 빠르게 수행하는 하나 이상의 특수화된 명령어(specialized instructions)도 포함됩니다. 각 특수화된 명령어는 입력이 예상대로일 때마다 증가하는 포화 카운터(saturating counter)를 유지합니다. 입력이 예상대로가 아니라면 카운터가 감소하고 일반 작업이 수행됩니다. 카운터가 최소값에 도달하면, 해당 명령어는 단순히 opcode를 적응형 버전으로 대체함으로써 비최적화(de-optimized)됩니다.
부대 데이터 (Ancillary data)
대부분의 특수화된 명령어 패밀리는 8비트 피연산자에 들어갈 수 있는 것보다 더 많은 정보를 필요로 합니다. 이를 위해 명령어 바로 뒤에 오는 여러 16비트 엔트리(entries)가 이 데이터를 저장하는 데 사용됩니다. 이는 인라인 캐시(inline cache)의 한 형태인 “인라인 데이터 캐시(inline data cache)”입니다. 특수화되지 않은(unspecialized) 또는 적응형(adaptive) 명령어는 이 캐시의 첫 번째 엔트리를 카운터로 사용하고 나머지는 단순히 건너뜁니다.
명령어 패밀리 예시 (Example families of instructions)
LOAD_ATTR
LOAD_ATTR
명령어는 스택 맨 위에 있는 객체의 이름이 지정된 속성(attribute)을 로드한 다음, 스택 맨 위에 있는 객체를 해당 속성으로 교체합니다.
이는 특수화의 명확한 후보입니다. 속성은 일반 인스턴스, 클래스, 모듈 또는 다른 많은 특수한 경우에 속할 수 있습니다.
LOAD_ATTR
는 처음에는 LOAD_ATTR_ADAPTIVE
로 quickening될 것입니다. 이는 실행 빈도를 추적하고, 충분히 실행되면 내부 함수인 _Py_Specialize_LoadAttr
를 호출하거나, 로드를 수행하기 위해 원래 LOAD_ATTR
명령어로 점프합니다. 최적화 시에는 속성의 종류가 검사되며, 적절한 특수화된 명령어가 발견되면 LOAD_ATTR_ADAPTIVE
를 그 자리에 대체합니다.
LOAD_ATTR
에 대한 특수화는 다음을 포함할 수 있습니다.
LOAD_ATTR_INSTANCE_VALUE
: 속성이 객체의 값 배열에 저장되고, 재정의하는 디스크립터(descriptor)에 의해 가려지지 않는 일반적인 경우.LOAD_ATTR_MODULE
: 모듈에서 속성을 로드합니다.LOAD_ATTR_SLOT
: 클래스가__slots__
를 정의하는 객체에서 속성을 로드합니다.
이것이 다른 최적화를 보완하는 최적화를 어떻게 가능하게 하는지 주목하십시오. LOAD_ATTR_INSTANCE_VALUE
는 많은 객체에 사용되는 “지연 딕셔너리(lazy dictionary)”와 잘 작동합니다.
LOAD_GLOBAL
LOAD_GLOBAL
명령어는 전역 네임스페이스(global namespace)에서 이름을 찾고, 전역 네임스페이스에 없으면 내장(builtins) 네임스페이스에서 찾습니다. Python 3.9에서는 LOAD_GLOBAL
의 C 코드가 전체 코드 객체를 수정하여 캐시를 추가해야 하는지, 전역 또는 내장 네임스페이스에서 캐시에 값을 조회하는 코드, 그리고 폴백 코드를 포함합니다. 이로 인해 코드가 복잡하고 부피가 커집니다. 또한, 최적화되었다고 해도 많은 중복 작업을 수행합니다.
명령어 패밀리를 사용하면 코드가 더 유지보수하기 쉽고 빨라집니다. 각 명령어는 하나의 관심사만 처리하면 되기 때문입니다.
특수화는 다음을 포함할 것입니다.
LOAD_GLOBAL_ADAPTIVE
: 위LOAD_ATTR_ADAPTIVE
와 유사하게 작동합니다.LOAD_GLOBAL_MODULE
: 값이 전역 네임스페이스에 있는 경우에 특수화될 수 있습니다. 네임스페이스의 키가 변경되지 않았는지 확인한 후, 저장된 인덱스에서 값을 로드할 수 있습니다.LOAD_GLOBAL_BUILTIN
: 값이 내장 네임스페이스에 있는 경우에 특수화될 수 있습니다. 전역 네임스페이스에 키가 추가되지 않았는지, 그리고 내장 네임스페이스가 변경되지 않았는지 확인해야 합니다. 전역 네임스페이스의 값이 변경되었는지 여부는 중요하지 않고, 키만 변경되었는지 여부가 중요하다는 점에 유의하십시오.
전체 구현은를 참조하십시오.
참고: 이 PEP는 특수화를 관리하기 위한 메커니즘을 설명하며, 적용될 특정 최적화를 명시하지는 않습니다. 코드가 더 개발됨에 따라 세부 사항 또는 전체 구현이 변경될 수 있습니다.
호환성 (Compatibility)
언어, 라이브러리 또는 API에 변경 사항은 없습니다.
사용자가 새 인터프리터의 존재를 감지할 수 있는 유일한 방법은 실행 시간 측정, 디버깅 도구 사용 또는 메모리 사용량 측정입니다.
비용 (Costs)
메모리 사용량 (Memory use)
어떤 종류의 캐싱을 수행하는 모든 체계에서 명백한 우려는 “얼마나 더 많은 메모리를 사용하는가?”입니다. 간단한 답변은 “그리 많지 않다”입니다.
3.10 버전과의 메모리 사용량 비교 (Comparing memory use to 3.10)
CPython 3.10은 명령어당 2바이트를 사용하다가 실행 횟수가 약 2000에 도달하면 명령어당 1바이트를 추가로 할당하고, 캐시(LOAD_GLOBAL 및 LOAD_ATTR)가 있는 명령어의 경우 명령어당 32바이트를 할당했습니다.
다음 표는 64비트 머신에서 3.10 opcache 또는 제안된 적응형 인터프리터를 지원하기 위한 명령어당 추가 바이트를 보여줍니다.
버전 | 3.10 cold | 3.10 hot | 3.11 Specialised |
---|---|---|---|
code | 2 | 2 | 2 |
opcache_map | 0 | 1 | 0 |
opcache/data | 0 | 4.8 | 4 |
Total | 2 | 7.8 | 6 |
3.10 cold
: 코드가 ~2000 한계에 도달하기 전.3.10 hot
: 임계값에 도달한 후의 캐시 사용량.
상대적인 메모리 사용량은 3.10에서 캐시 생성을 트리거할 만큼 “핫(hot)”한 코드의 양에 따라 달라집니다. 3.10과 3.11이 사용하는 메모리량이 동일해지는 손익분기점은 약 70%입니다.
실제 바이트코드는 코드 객체의 일부일 뿐이라는 점도 주목할 가치가 있습니다. 코드 객체에는 이름, 상수 및 상당한 양의 디버깅 정보도 포함됩니다.
요약하면, 많은 함수가 상대적으로 사용되지 않는 대부분의 애플리케이션에서 3.11은 3.10보다 더 많은 메모리를 소비하겠지만, 그 차이는 크지 않을 것입니다.
보안 영향 (Security Implications)
없음.
기각된 아이디어 (Rejected Ideas)
인라인 데이터 캐시(inline data caches)를 갖춘 특수화 적응형 인터프리터를 구현함으로써, CPython을 최적화하는 많은 대체 방법들을 암묵적으로 기각하고 있습니다. 그러나 JIT 컴파일과 같은 일부 아이디어는 단순히 보류되었을 뿐, 기각된 것은 아니라는 점을 강조할 가치가 있습니다.
바이트코드 전에 데이터 캐시 저장 (Storing data caches before the bytecode)
3.11 알파 버전의 이 PEP 초기 구현은 아래 설명된 것과 다른 캐싱 방식을 사용했습니다.
- Quickened 명령어는 원본 바이트코드와 동일한 형식으로 배열에 저장됩니다 (Python 객체에 저장할 필요도 없고 바람직하지도 않습니다). 부대 데이터(ancillary data)는 별도의 배열에 저장됩니다.
- 각 명령어는 0개 이상의 데이터 엔트리를 사용합니다. 패밀리 내의 각 명령어는 동일한 양의 데이터가 할당되어야 하지만, 일부 명령어는 모든 데이터를 사용하지 않을 수도 있습니다.
POP_TOP
과 같이 특수화될 수 없는 명령어는 엔트리가 필요하지 않습니다. 실험에 따르면 명령어의 25%~30%가 유용하게 특수화될 수 있습니다. 다른 패밀리는 다른 양의 데이터를 필요로 하지만, 대부분은 2개의 엔트리(64비트 머신에서 16바이트)가 필요합니다. - 256개 명령어보다 큰 함수를 지원하기 위해, 명령어의 첫 번째 데이터 엔트리 오프셋은
(instruction offset)//2 + (quickened operand)
로 계산합니다.
Python 3.10의 opcache와 비교할 때, 이 설계는 다음과 같은 특징을 가집니다.
- 더 빠릅니다. 오프셋을 계산하기 위해 메모리 읽기가 필요하지 않습니다. 3.10은 두 번의 종속적인 읽기가 필요합니다.
- 데이터가 다른 명령어 패밀리에 대해 다른 크기를 가질 수 있고, 추가적인 오프셋 배열이 필요 없으므로 메모리 사용량이 훨씬 적습니다.
- 함수당 약 5000개 명령어까지 훨씬 더 큰 함수를 지원할 수 있습니다. 3.10은 약 1000개를 지원합니다.
이 방식은 인라인 캐시 접근 방식이 더 빠르고 간단하기 때문에 기각되었습니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments