PEP 611 – 백만 제한
- Author:
- Mark Shannon <mark at hotpy.org>
- Status:
- Withdrawn
- Type:
- Standards Track
- Created:
- 05-Dec-2019
- Post-History:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PR은 다양한 Python 코드 및 그 구현 측면에 대해 백만(1 000 000)의 소프트 제한과 그보다 큰 하드 제한을 제안합니다.
Python 언어는 많은 기능에 대한 제한을 지정하지 않습니다. 이러한 값에 아무런 제한도 두지 않는 것은 적어도 표면적으로는 프로그래머의 자유를 향상시키는 것처럼 보이지만, 실제로는 CPython VM과 기타 Python 가상 머신에 암묵적인 제한이 있거나 제한이 천문학적으로 크다고 가정해야 하므로 비용이 많이 듭니다.
이 PR은 백만의 제한을 적용할 여러 기능을 나열합니다.
CPython의 하드 제한은 팔백만(8 000 000)이 됩니다.
동기
가상 머신에서 표현해야 하는 값은 많습니다. 이러한 값에 대한 제한이 지정되지 않으면 표현 방식이 비효율적이거나 오버플로에 취약해야 합니다. CPython 가상 머신은 줄 번호, 스택 오프셋 및 명령어 오프셋과 같은 값을 32비트 값으로 표현합니다. 이는 비효율적이며 잠재적으로 안전하지 않습니다.
실제 값은 이를 표현하는 데 대개 12비트 남짓보다 많이 필요하지 않으므로 비효율적입니다.
악의적이거나 잘못 생성된 코드가 값이 232보다 커지게 만들 수 있으므로 안전하지 않습니다.
예를 들어 줄 번호는 내부적으로 32비트 값으로 표현됩니다. 모듈은 거의 절대로 수천 줄을 넘지 않으므로 이는 비효율적입니다. 비효율적임에도 불구하고 공격자가 수십억 개의 개행 문자를 포함하는 모듈을 쉽게 생성할 수 있으므로 여전히 오버플로에 취약합니다.
메모리 접근은 일반적으로 최신 CPU 성능의 제한 요소입니다. 데이터 구조를 더 효율적으로 패킹하면 지역성이 향상되고 메모리 대역폭이 감소하며, ALU 사용량(시프트 및 마스킹을 위한 사용량)은 소폭 증가합니다. 중요한 값을 20비트에 안전하게 저장할 수 있으면 다음을 포함하되 이에 국한되지 않는 여러 데이터 구조에서 메모리를 절약할 수 있습니다.
- 프레임 객체
- 객체 헤더
- 코드 객체
더 효율적인 명령어 형식의 가능성도 있으며, 이를 통해 인터프리터 디스패치 속도를 높일 수 있습니다.
이것은 감수할 만한 절충입니까?
어떤 형태의 제한이든 단점은 잠재적으로 누군가의 작업을 더 어렵게 만들 수 있다는 것입니다. 예를 들어, 모듈의 크기를 100만 줄로 유지하는 코드 제너레이터를 작성하기가 더 어려울 수 있습니다. 그러나 많은 코드 생성기를 작성해 본 저자의 견해로는, 그러한 제한이 실제로 문제가 될 가능성은 극히 낮습니다.
이러한 제한의 장점은 CPython, PyPy 또는 기타 어떤 구현이든 런타임 구현자가 성능을 개선할 수 있는 자유를 부여한다는 점입니다. 전 세계적으로 Python 프로그램을 실행하는 비용을 0.1%만 줄여도 얻을 수 있는 잠재적 가치가 소수의 코드 생성기를 수정하는 비용을 크게 초과할 것이라는 것이 저자의 믿음입니다.
근거
모듈의 코드 줄 수와 지역 변수 수와 같은 값에 제한을 부과하면 가상 머신을 쉽게 구현하고 효율적으로 만드는 데 상당한 이점이 있습니다. 제한이 충분히 크다면 언어 사용자에게 부정적인 영향은 없습니다.
이 값들에 대해 고정되어 있지만 큰 제한을 선택하면, 인간 프로그래머에게 불편을 초래하지 않으면서 안전성과 효율성을 모두 확보할 수 있고 코드 생성기에는 매우 드문 문제만 발생하게 할 수 있습니다.
백만
“백만”이라는 값은 기억하기가 매우 쉽습니다.
백만 제한은 런타임 크기가 아니라 대부분 사람이 생성한 코드에 대한 제한입니다.
단일 모듈에 백만 줄의 코드가 들어가는 것은 터무니없는 코드 집중입니다. 전체 Python 표준 라이브러리도 약 백만 줄의 3분의 2에 해당하며, 1600개 파일에 분산되어 있습니다.
Java Virtual Machine (JVM) [1]은 여기에서 다루는 것과 유사한 많은 프로그램 요소에 대해 216-1 (65535)의 제한을 지정합니다. 이 제한을 사용하면 제한된 값이 16비트에 들어갈 수 있으며, 이는 매우 효율적인 기계 표현입니다. 그러나 실제로는 코드 생성기가 이 제한을 상당히 쉽게 초과하며, 작성자는 이미 216줄의 코드를 초과하는 기존 Python 코드를 알고 있습니다.
800만이라는 하드 제한은 23비트에 들어가며, 기계 표현으로는 그다지 편리하지 않지만 여전히 합리적으로 간결합니다. 800만이라는 제한은 효율성 측면의 이점을 얻을 만큼 충분히 작고(23비트만 필요함), 사용자에게 영향을 주지 않을 만큼 충분히 큽니다(누구도 그만큼 큰 모듈을 작성한 적이 없습니다).
생성된 코드가 제한을 초과할 가능성은 있지만, 코드 제너레이터가 이에 맞도록 출력을 수정하는 것은 쉽습니다. 작성자는 Java 코드를 생성하면서 JVM의 64K 제한에 적어도 두 번 도달한 적이 있습니다. 해결 방법은 비교적 간단했으며, 바이트코드 또는 코드 줄 수에 백만이라는 제한이 있었다면 필요하지 않았을 것입니다.
필요한 경우 백만 제한을 초과하는 프로그램에 대해서는 소프트 제한을 늘릴 수 있습니다.
백만이라는 소프트 제한을 두면 오류를 발생시키거나 즉각적인 수정을 강제하지 않고도 문제가 있는 코드에 대한 경고를 제공할 수 있습니다. 또한 동적 최적화 프로그램이 인라인 검사 없이 더 간결한 형식을 사용할 수 있게 합니다.
명세
이 PR은 다음 언어 기능과 런타임 값에 백만이라는 소프트 제한을 둘 것을 제안합니다.
- 모듈의 소스 코드 줄 수
- 코드 객체의 바이트코드 명령어 수
- 코드 객체의 지역 변수 수와 스택 사용량의 합
- 실행 중인 인터프리터의 클래스 수
- Python 코드의 재귀 깊이
클래스 수가 백만에 도달하기 전에 메모리 제약이 제한 요인이 될 가능성이 높습니다.
재귀 깊이
재귀 깊이 제한은 순수 Python 코드에만 적용됩니다. C와 같은 외부 언어로 작성된 코드는 하드웨어 스택을 사용할 수 있으므로 재귀 깊이가 수천 정도로 제한될 수 있습니다. 하드웨어 스택이 제한에 가까워지면 구현이 예외를 발생시킬 것으로 예상합니다. Python 호출과 C 호출이 섞인 코드에서는 하드웨어 제한이 먼저 적용될 가능성이 가장 높습니다. 하드웨어 재귀의 크기는 런타임에 달라질 수 있으며 표시되지 않습니다.
소프트 및 하드 제한
하드 제한이 소프트 제한과 동일한 값을 갖는 경우를 제외하고, 구현은 소프트 제한을 초과할 때마다 경고를 발생해야 합니다. 하드 제한을 초과하면 예외를 발생해야 합니다.
구현에 따라 서로 다른 하드 제한이 적용될 수 있습니다. 일부 경우에는 하드 제한이 소프트 제한보다 낮을 수 있습니다. 예를 들어, 많은 MicroPython 포트는 이처럼 큰 제한을 지원하지 못할 가능성이 높습니다.
제한 조사 및 수정
런타임에 소프트 제한을 조사하거나 수정할 수 있도록 하나 이상의 함수가 sys 모듈에 제공되지만, 제한을 하드 제한보다 높게 설정할 수는 없습니다.
추론된 제한
이러한 제한은 사양의 일부가 아니지만, 코드 객체의 바이트코드 명령어 수 제한으로부터 백만 미만의 제한을 추론할 수 있습니다. 백만 개를 초과하는 상수를 불러오거나 백만 개를 초과하는 이름을 사용할 만큼 명령어가 충분하지 않기 때문입니다.
- 코드 객체에 있는 서로 다른 이름의 수입니다.
- 코드 객체에 있는 상수의 수입니다.
이러한 제한을 적용할 때 CPython의 장점은 다음과 같습니다:
모듈의 코드 줄 및 코드 객체 제한입니다.
소스 코드를 바이트코드로 컴파일하거나 프로파일링 또는 디버깅을 위해 바이트코드를 수정할 때 중간 형식이 필요합니다. 피연산자를 23비트로 제한하면 명령어를 간결한 64비트 형식으로 표현할 수 있어 명령어 시퀀스를 매우 빠르게 처리할 수 있습니다.
23비트 피연산자(상대 분기의 경우 24비트)를 사용하면 추가적인 EXTENDED_ARG 명령어 없이도 명령어를 32비트에 맞출 수 있습니다. 피연산자가 해당 명령어에 엄격히 국한되므로 디스패치 성능이 향상됩니다. 이것이 성능 향상에 도움이 될지는 분명하지 않으며, 단지 가능한 방식을 보여 주는 예일 뿐입니다.
모듈의 줄 수를 제한하는 이점은 주로 바이트코드에 내재된 제한에 있습니다. 구현에서 모듈당 줄 수가 아니라 코드 객체당 명령어 수를 백만 개로 제한하는 것이 더 중요하지만, 백만 줄 제한이라고 설명하는 편이 훨씬 쉽습니다. 일관되게 백만으로 제한하면 기억하기가 더 쉽습니다. 보장되지는 않지만 줄 제한에 먼저 도달할 가능성이 가장 높으므로 개발자가 이해하기 더 쉬운 오류 메시지를 제공할 수 있습니다.
실행 중인 인터프리터의 전체 클래스 수
이 제한은 객체 헤더의 크기를 상당히 줄일 가능성이 있습니다.
현재 객체는 참조가 없는 객체(int, float, str 등)의 경우 2워드 헤더를 가지며, 참조가 있는 객체의 경우 4워드 헤더를 가집니다. 클래스의 최대 수를 줄이면 클래스 참조에 필요한 공간을 64비트에서 32비트 미만으로 줄일 수 있어 훨씬 더 간결한 헤더를 사용할 수 있습니다.
예를 들어, 초간결 헤더 형식은 다음과 같을 수 있습니다:
struct header {
uint32_t gc_flags:6; /* Needs finalisation, might be part of a cycle, etc. */
uint32_t class_id:26; /* Can be efficiently mapped to address by ensuring suitable alignment of classes */
uint32_t refcount; /* Limited memory or saturating */
}
이 형식을 사용하면 64비트 머신에서 슬롯이 없는 Python 객체의 크기를 40바이트에서 16바이트로 줄일 수 있습니다.
64비트 머신에서 32비트 참조 카운트를 사용하는 방법은 두 가지가 있다는 점에 유의하십시오. 한 가지 방법은 각 서브 인터프리터를 32GB 메모리로 제한하는 것입니다. 다른 방법은 포화 참조 카운트를 사용하는 것으로, 약간 느려질 수 있지만 메모리 할당을 무제한으로 허용합니다.
적용
Python 구현은 이러한 제한을 적용할 의무가 없습니다. 그러나 성능 저하 없이 제한을 적용할 수 있다면 적용해야 합니다.
CPython은 다음과 같이 제한을 적용할 것으로 예상됩니다.
- 모듈의 소스 코드 줄 수: 버전 3.9부터입니다.
- 코드 객체의 바이트코드 명령어 수: 3.9부터입니다.
- 코드 객체의 지역 변수 수와 스택 사용량의 합: 3.9부터입니다.
- 실행 중인 인터프리터의 클래스 수: 아마 3.10부터이며, 3.9에서는 경고가 발생할 수도 있습니다.
CPython의 엄격한 제한
CPython은 위의 모든 값에 엄격한 제한을 적용합니다. 엄격한 제한의 값은 800만입니다.
일부 기계 생성 코드가 위의 제한 중 하나 이상을 초과하는 일이 이론적으로 가능할 수 있습니다. 저자는 그러한 일이 일어날 가능성이 매우 낮으며 코드 제너레이터의 출력 단계를 수정하면 쉽게 해결할 수 있다고 생각합니다.
성능을 위해 위 제한의 이점을 가능한 한 빨리 얻고자 합니다. 이를 위해 CPython은 버전 3.9부터 제한을 적용하기 시작합니다. 전환을 원활하게 하고 호환성 문제를 최소화하기 위해 초기 제한은 1,600만으로 설정하고, 이후 버전에서 800만으로 줄입니다.
하위 호환성
CPython이 실제로 적용하는 엄격한 제한은 다음과 같습니다.
| 버전 | 엄격한 제한 |
|---|---|
| 3.9 | 1,600만 |
| 3.10부터 | 800만 |
100만 제한을 초과하는 코드 생성기가 드물고 일반적으로 사용되는 환경을 고려하면, 제한 대상 수량이 100만을 초과할 경우 3.9부터 경고를 발행하기 시작하는 것이 합리적으로 보입니다.
역사적으로 재귀 제한은 1000으로 설정되어 왔습니다. 값이 작다는 점에 암묵적으로 의존하는 코드가 손상되는 것을 방지하기 위해 완화된 재귀 제한을 다음과 같이 점진적으로 늘립니다.
| 버전 | 완화된 제한 |
|---|---|
| 3.9 | 4 000 |
| 3.10 | 16 000 |
| 3.11 | 64 000 |
| 3.12 | 125 000 |
| 3.13 | 100만 |
하드 제한은 즉시 800만으로 설정합니다.
기타 구현
CPython 이외의 Python 구현은 목적이 다르므로 서로 다른 제한이 적절할 수 있습니다. 제한이 명확하게 문서화되어 있다면 허용됩니다.
범용 구현
PyPy와 같은 범용 구현체는 백만 개 한도를 사용해야 합니다. 최대 호환성이 목표라면, 3.9에서 3.11까지의 CPython 동작도 따라야 합니다.
특수 목적 구현체
특수 목적 구현체는 명확히 문서화되어 있는 한 더 낮은 한도를 사용할 수 있습니다. 임베디드 시스템을 위해 설계된 구현체, 예를 들어 MicroPython은 수천 개 정도로 낮은 한도를 부과할 수 있습니다.
보안 영향
미미합니다. 이는 모든 파이썬 가상 머신의 공격 표면을 조금 줄입니다.
참조 구현
아직 없습니다. PEP가 승인되면 CPython에 구현될 것입니다.
거부된 아이디어
컴파일 시점에 상한을 위로 수정할 수 있게 하자는 제안이 Tal Einat에 의해 제기되었습니다. 현재 232의 한도가 문제가 된 적이 없고, 220과 232 사이의 한도를 허용하는 실질적 이점이 이러한 기능을 지원하는 데 드는 추가적인 코드 복잡도에 비해 미미해 보이므로 이 제안은 거부되었습니다.
미해결 문제
아직 없습니다.
참고 문헌
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.