Skip to content

필사 모드: CPython 바이트코드 인터프리터 Deep Dive — ceval.c, Specializing Adaptive, PEP 659, Copy-and-Patch JIT 완전 정복 (2025)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

TL;DR

- **CPython**은 "Python 소스 → AST → 바이트코드 → `ceval.c` 인터프리터" 파이프라인으로 실행된다. "해석 언어"라지만 실제로는 **컴파일 후 VM 실행**.

- **Bytecode**: 스택 기반 VM 명령어. `LOAD_FAST`, `BINARY_ADD`, `CALL_FUNCTION` 등. `.pyc` 파일에 저장.

- **ceval.c**: Python의 심장. 거대한 switch 또는 computed-goto 루프. 모든 Python 코드가 여기를 거친다.

- **Python 3.11+ PEP 659**: Specializing Adaptive Interpreter. 핫 경로를 런타임에 특수화된 opcode로 교체 → **25%+ 성능 향상**.

- **Inline Cache**: 각 opcode 뒤에 **cache 슬롯**. 첫 실행 시 프로파일 → 특수화 → 후속 실행 빠름.

- **Python 3.12 PEP 684**: Per-interpreter GIL. 서브인터프리터마다 독립 GIL → 병렬성 개선.

- **Python 3.13 PEP 703**: Free-threaded build. **GIL 제거** (실험적). 아직 기본 아님.

- **Python 3.13 PEP 744**: Copy-and-patch JIT. Tier 2 optimizer. 템플릿 기반 초고속 컴파일.

- **Frame Object**: 각 함수 호출의 실행 컨텍스트. 로컬/스택/명령 포인터/예외.

- **dis 모듈**: 바이트코드 확인. `dis.dis(f)`로 함수의 명령어 출력.

1. Python이 "느린" 이유

1.1 동적 타입의 대가

Python의 단순한 덧셈:

x + y

C에서 `int + int`는 CPU 명령어 **1개**(`add`). Python에서는?

1. `x`와 `y`의 타입 확인.

2. `x`의 `__add__` 메서드 검색.

3. 없으면 `y`의 `__radd__` 검색.

4. 메서드 호출.

5. 결과 객체 생성 (힙 할당).

6. 레퍼런스 카운트 조정.

수십 개의 C 명령어. 100배 이상 느리다.

1.2 Boxing

Python의 모든 값은 **객체**. `int` 하나도 `PyLongObject` 힙 객체:

typedef struct _longobject {

PyObject_VAR_HEAD

digit ob_digit[1];

} PyLongObject;

32 바이트 이상의 구조체. "1"도 이 크기.

1.3 Reference Counting

CPython의 메모리 관리는 **참조 카운트** 기반:

Py_INCREF(obj); // 카운트 +1

Py_DECREF(obj); // 카운트 -1, 0이면 해제

모든 객체 할당/해제에 이 오버헤드. 멀티스레드에선 atomic 필요 → 더 느림. GIL이 있는 이유 중 하나.

1.4 "그래서 Python은 영원히 느려야 하나?"

아니다. 2021년 이후 CPython은 **실질적 성능 혁명** 중이다:

- **Python 3.11** (2022 10월): 평균 25% 빠름.

- **Python 3.12** (2023): 추가 5%.

- **Python 3.13** (2024): Free-threaded + JIT (실험).

- **Python 3.14** (2025 예정): 더 공격적 최적화.

"Faster CPython" 프로젝트 (Guido van Rossum이 Microsoft 합류 후 시작). 목표: **Python 3.10 대비 5배 빠름**.

2. 컴파일 파이프라인

2.1 단계

Python source (hello.py)

Lexer (tokenizer)

Parser

AST (abstract syntax tree)

Compiler (ast → bytecode)

Bytecode (code object)

ceval.c interpreter

실행

**놀라움**: Python도 "컴파일"한다. "해석 언어"라는 말은 기계어가 아닌 VM 명령어로 컴파일된다는 의미.

2.2 Lexer & Parser

Python 3.9+는 **PEG parser** (Pegen). 이전엔 LL(1) parser. PEG는 더 유연.

code = "x = 1 + 2\n"

tokens = list(tokenize.tokenize(io.BytesIO(code.encode()).readline))

for t in tokens:

print(t)

TokenInfo(type=ENCODING, string='utf-8', ...)

TokenInfo(type=NAME, string='x', ...)

TokenInfo(type=OP, string='=', ...)

TokenInfo(type=NUMBER, string='1', ...)

TokenInfo(type=OP, string='+', ...)

...

2.3 AST

파서의 출력은 **AST**:

tree = ast.parse("x = 1 + 2")

print(ast.dump(tree, indent=2))

Module(

body=[

Assign(

targets=[Name(id='x', ctx=Store())],

value=BinOp(

left=Constant(value=1),

op=Add(),

right=Constant(value=2)))],

type_ignores=[])

AST는 소스와 거의 1:1. `ast` 모듈로 조작 가능 → AST transform, macro 유사 기능.

2.4 Code Object

Compiler가 AST를 **code object**로 변환.

def add(a, b):

return a + b

print(add.__code__)

print(add.__code__.co_code) # 바이트코드 (bytes)

print(add.__code__.co_consts) # 상수들

print(add.__code__.co_names) # 글로벌 이름

print(add.__code__.co_varnames) # 로컬 이름

`PyCodeObject` 구조체:

- `co_code`: 바이트코드.

- `co_consts`: 상수 풀 (리터럴, docstring 등).

- `co_names`: 전역/attr 이름.

- `co_varnames`: 로컬 변수 이름.

- `co_flags`: 특수 플래그 (async, generator 등).

- `co_lnotab`: 라인 번호 테이블 (바이트코드 offset → 소스 라인).

- `co_stacksize`: 최대 스택 깊이.

2.5 .pyc 파일

Code object는 **marshal** 형식으로 `.pyc` 파일에 저장. `__pycache__/` 디렉토리.

`.pyc` 구조:

magic number (버전)

source mtime or hash

source size

marshaled code object

재실행 시 Python이 `.py`와 `.pyc` 비교 → 변경 없으면 `.pyc` 로드 (파싱 skip).

**구조 변경**: Python 3.7+는 `SOURCE_DATE_EPOCH`를 고려한 **reproducible** `.pyc` 지원.

3. 바이트코드 해부

3.1 dis 모듈

Python 코드의 바이트코드를 사람이 읽을 수 있게 출력:

def add(a, b):

return a + b

dis.dis(add)

2 0 RESUME 0

3 2 LOAD_FAST 0 (a)

4 LOAD_FAST 1 (b)

6 BINARY_OP 0 (+)

10 RETURN_VALUE

각 줄:

- 소스 라인 번호 (왼쪽).

- 바이트 offset.

- Opcode 이름.

- Operand (숫자).

- 주석 (dis가 추가).

3.2 Opcode

Python 3.12 기준 ~200개 opcode. 주요 카테고리:

**스택 조작**:

- `LOAD_CONST`: 상수를 스택에 push.

- `LOAD_FAST`: 로컬 변수.

- `LOAD_GLOBAL`: 전역.

- `LOAD_ATTR`: 속성 (obj.attr).

- `STORE_FAST`: 로컬 변수 저장.

- `POP_TOP`: 스택 top 제거.

- `DUP_TOP`: top 복제.

**산술**:

- `BINARY_OP`: +, -, *, /, // 등 (Python 3.11부터 통합).

- `COMPARE_OP`: `<`, `>`, `==`.

**제어 흐름**:

- `JUMP_FORWARD`: 무조건 점프.

- `POP_JUMP_IF_FALSE`: 조건 점프.

- `FOR_ITER`: 반복자 next.

**함수 호출**:

- `CALL`: 함수 호출.

- `RETURN_VALUE`: 반환.

- `MAKE_FUNCTION`: 함수 객체 생성.

**객체 생성**:

- `BUILD_LIST`, `BUILD_TUPLE`, `BUILD_DICT`.

3.3 스택 기반

Python VM은 **스택 기반**. 레지스터 없이 스택에서만 작동.

`a + b` 실행:

LOAD_FAST 0 # 스택: [a]

LOAD_FAST 1 # 스택: [a, b]

BINARY_OP 0 # a, b pop → a+b push. 스택: [a+b]

RETURN_VALUE # 스택 top 반환.

스택 기반의 장점:

- **명령어 단순**: operand 위치를 명시할 필요 없음.

- **컴파일 쉬움**: AST → 스택 push/pop 순차 생성.

단점:

- **레지스터 기반보다 명령어 수 많음**: 각 값이 push/pop 왕복.

- **JIT에 덜 친화적**: 레지스터 기반이 더 최적화 쉬움.

Lua, JVM(Java), .NET(CLR)은 스택 기반. Dalvik(Android)은 레지스터 기반.

3.4 Python 3.11의 변화

Python 3.11 이전:

LOAD_FAST 0

LOAD_FAST 1

BINARY_ADD # BINARY_SUB, BINARY_MULT 등 각자

Python 3.11+:

LOAD_FAST 0

LOAD_FAST 1

BINARY_OP 0 # 0=+, 5=*, 등. 통합된 하나

**통합의 이유**: PEP 659 specialization을 쉽게 하기 위해. 모든 이진 연산이 하나의 "적응형" opcode를 거침.

3.5 더 복잡한 예

def fib(n):

if n < 2:

return n

return fib(n-1) + fib(n-2)

2 0 RESUME 0

3 2 LOAD_FAST 0 (n)

4 LOAD_CONST 1 (2)

6 COMPARE_OP 40 (<)

10 POP_JUMP_IF_FALSE 6 (to 24)

4 12 LOAD_FAST 0 (n)

14 RETURN_VALUE

5 >> 24 LOAD_GLOBAL 1 (NULL + fib)

36 LOAD_FAST 0 (n)

38 LOAD_CONST 1 (2)

40 BINARY_OP 10 (-)

44 CALL 1

52 LOAD_GLOBAL 1 (NULL + fib)

64 LOAD_FAST 0 (n)

66 LOAD_CONST 2 (1)

68 BINARY_OP 10 (-)

72 CALL 1

80 BINARY_OP 0 (+)

84 RETURN_VALUE

재귀 호출, 조건 분기, 산술의 조합.

4. ceval.c — Python의 심장

4.1 파일 개요

`Python/ceval.c`는 **CPython에서 가장 중요한 파일**. 대략 **8,000 줄** (Python 3.12 기준).

주요 함수:

- `_PyEval_EvalFrameDefault()`: 메인 인터프리터 루프.

- `PyEval_EvalCode()`: 최상위 code 실행.

4.2 Frame 실행

Python 함수가 호출되면:

1. 새 **frame object** 생성.

2. 로컬 변수 초기화.

3. `_PyEval_EvalFrameDefault(frame)` 호출.

4. 루프 실행.

5. 리턴 시 frame 해제.

typedef struct _PyInterpreterFrame {

PyFunctionObject *f_funcobj;

PyObject *f_builtins;

PyObject *f_globals;

PyObject *f_locals;

PyCodeObject *f_code;

PyObject *frame_obj;

struct _PyInterpreterFrame *previous;

_Py_CODEUNIT *prev_instr; // 현재 명령어

int stacktop;

bool is_entry;

char owner;

PyObject *localsplus[1]; // 로컬 + 스택 (flex array)

} _PyInterpreterFrame;

`localsplus`가 핵심. 로컬 변수와 evaluation stack이 하나의 배열에 붙어있다 (성능 이유).

4.3 Main Loop 구조

단순화된 pseudo code:

PyObject *

_PyEval_EvalFrameDefault(_PyInterpreterFrame *frame, int throwflag)

{

_Py_CODEUNIT *next_instr = frame->prev_instr + 1;

PyObject **stack_pointer = _PyFrame_GetStackPointer(frame);

while (1) {

_Py_CODEUNIT word = *next_instr++;

int opcode = _Py_OPCODE(word);

int oparg = _Py_OPARG(word);

switch (opcode) {

case LOAD_FAST: {

PyObject *value = frame->localsplus[oparg];

Py_INCREF(value);

*stack_pointer++ = value;

DISPATCH();

}

case LOAD_CONST: {

PyObject *value = PyTuple_GET_ITEM(

frame->f_code->co_consts, oparg);

Py_INCREF(value);

*stack_pointer++ = value;

DISPATCH();

}

case BINARY_OP: {

PyObject *right = *(--stack_pointer);

PyObject *left = *(--stack_pointer);

PyObject *result = binary_op(left, right, oparg);

Py_DECREF(left);

Py_DECREF(right);

if (result == NULL) goto error;

*stack_pointer++ = result;

DISPATCH();

}

case RETURN_VALUE: {

PyObject *retval = *(--stack_pointer);

// frame 정리

return retval;

}

// ... 수백 개 case

}

}

}

**거대한 switch**. 각 opcode가 자기 handler.

4.4 Computed GOTO

`switch` 기반은 느릴 수 있다. 각 iter마다:

1. `next_instr++` 읽기.

2. `switch` 테이블 조회.

3. 점프.

분기 예측 실패가 많다. GCC와 Clang은 **computed goto** 확장 지원:

#define DISPATCH() goto *opcode_targets[opcode];

static void *opcode_targets[] = {

[LOAD_FAST] = &&LOAD_FAST_LABEL,

[LOAD_CONST] = &&LOAD_CONST_LABEL,

...

};

LOAD_FAST_LABEL:

// ... handler ...

DISPATCH();

LOAD_CONST_LABEL:

// ...

DISPATCH();

Switch 대신 **직접 점프**. CPU의 branch predictor가 각 opcode 별로 **독립된 history**를 가짐. Switch 기반보다 10-15% 빠름.

Python은 `USE_COMPUTED_GOTOS` 매크로로 컴파일 시 결정. GCC/Clang에서 기본 활성화.

4.5 Dispatch Overhead

이 루프의 오버헤드는 사소하지 않다. 각 opcode 실행 시:

- fetch.

- decode.

- execute (작은 경우).

- branch predict.

각 Python 명령어가 수 ns. 1,000,000 명령어 실행 = 수 ms의 "오버헤드만".

C 코드는 같은 일을 수백 ns에 한다. 이것이 Python 속도의 큰 부분.

5. Frame과 호출 스택

5.1 Frame의 역할

각 함수 호출이 새 frame. Frame은 "실행 중인 함수 하나의 상태".

def outer():

x = 1

inner()

def inner():

print("hi")

outer()

실행 중:

Frame stack:

inner frame (top)

outer frame

`inner`가 리턴하면 그 frame pop, `outer` frame으로 제어 이동.

5.2 Python 3.11의 Frame 개선

Python 3.11에서 frame 할당 비용 대폭 감소:

- **이전**: 각 frame이 heap 객체 (malloc).

- **3.11+**: frame이 **C stack**에 할당. 필요할 때만 heap.

결과: 함수 호출 50% 빠름.

5.3 traceback과 frame

Exception 발생 시 traceback은 frame stack을 탐색:

def inner():

raise ValueError("oops")

def outer():

inner()

try:

outer()

except:

traceback.print_exc()

Traceback (most recent call last):

File "a.py", line 8, in <module>

outer()

File "a.py", line 6, in outer

inner()

File "a.py", line 3, in inner

raise ValueError("oops")

각 줄이 frame에 대응.

5.4 sys._getframe

frame = sys._getframe()

print(frame.f_code.co_name) # 현재 함수 이름

print(frame.f_locals) # 로컬 변수 dict

print(frame.f_back) # 호출자 frame

디버거, 프로파일러, logging 라이브러리가 활용.

5.5 f-string과 Frame

f-string `f"{x}"`는 컴파일 타임에 해석되지만, **값은 런타임 frame의 로컬**. 그래서 `f"{name}"`은 현재 frame에서 `name`을 찾음.

6. Specializing Adaptive Interpreter (PEP 659)

Python 3.11의 가장 큰 혁신. Faster CPython 프로젝트의 핵심.

6.1 문제

`LOAD_ATTR` opcode를 생각해보자:

def get(obj):

return obj.name

`obj.name` 실행 시 CPython이 하는 일:

1. `obj`의 `type`의 MRO 순회.

2. `name`을 descriptor로 찾기.

3. 찾으면 descriptor protocol.

4. 못 찾으면 `obj.__dict__` 조회.

5. `__getattr__` fallback.

**복잡하다**. 그런데 대부분의 경우:

- `obj`가 같은 타입.

- `name`이 `obj.__dict__`의 직접 키.

캐시할 수 있지 않나?

6.2 Inline Cache

**Inline cache**는 각 opcode 옆에 **cache slot**을 두는 기법. JavaScript의 V8, Java의 HotSpot이 사용.

CPython 3.11+: **adaptive opcodes**.

LOAD_ATTR 0 ← 일반 opcode

첫 실행 시:

- 프로파일링: obj type, offset 기록

- Opcode를 LOAD_ATTR_INSTANCE_VALUE로 교체 (내부에서만)

다음 실행 시:

- 새로운 specialized 버전 사용

- Type 확인 (가정한 type과 같은가?)

- 같으면: 직접 offset으로 접근 (매우 빠름)

- 다르면: 원래 LOAD_ATTR로 fallback

6.3 Inline Cache 구조

[LOAD_ATTR opcode (2 bytes)] [cache slot (여러 바이트)]

Cache slot에는:

- 예상 type의 version tag.

- Dict offset 또는 descriptor 캐시.

- 통계 (hit/miss).

이것이 **inline** 이유: cache가 바이트코드 스트림 자체에 포함.

6.4 Specialization 예시

**LOAD_ATTR**의 specialized variants:

- `LOAD_ATTR_INSTANCE_VALUE`: `obj.__dict__`에 직접 있는 속성.

- `LOAD_ATTR_WITH_HINT`: dict offset hint 사용.

- `LOAD_ATTR_SLOT`: `__slots__` 기반.

- `LOAD_ATTR_MODULE`: 모듈 전역.

- `LOAD_ATTR_CLASS`: 클래스 속성.

- `LOAD_ATTR_METHOD_WITH_VALUES`: method + instance 동시.

- `LOAD_ATTR_PROPERTY`: property descriptor.

각자 별도 C 코드 경로 → **가정이 맞으면 매우 빠름**.

6.5 BINARY_OP 특수화

BINARY_OP_ADD_INT

BINARY_OP_ADD_FLOAT

BINARY_OP_ADD_UNICODE

BINARY_OP_MULTIPLY_INT

BINARY_OP_SUBTRACT_INT

...

`x + y`가 항상 int라면 `BINARY_OP_ADD_INT`로 교체. 타입 체크 후 바로 int 덧셈. 10배 이상 빠름.

6.6 De-optimization

가정이 틀리면?

def add(a, b):

return a + b

add(1, 2) # → BINARY_OP_ADD_INT

add(1.0, 2.0) # → INT 가정 실패 → 원래 BINARY_OP로 fallback

일정 실패 횟수 후 opcode를 de-optimize. 하지만 re-specialize도 가능.

6.7 성능 결과

Python 3.11 릴리스 노트:

- **pyperformance 벤치마크**: 평균 **25%+ 빠름**.

- 일부 벤치마크: 60% 이상.

- Django 템플릿 렌더링: 15% 빠름.

실제 코드에서 눈에 띄는 차이. 그리고 **사용자는 코드를 전혀 바꿀 필요 없다**.

6.8 성공의 의미

PEP 659는 **JIT 없이** 성능 향상을 증명했다. "Python을 빠르게 하려면 JIT이 필요하다"는 통념 반박. 단순 인터프리터 최적화로도 큰 이득.

그렇다고 JIT이 불필요한 건 아니다 — 다음 단계. 하지만 JIT 없이도 갈 길이 많이 남았음을 보임.

7. Python 3.12 — Per-Interpreter GIL

7.1 Sub-Interpreters

CPython에는 오래된 기능이 있다: **서브인터프리터**. 한 프로세스 안에 여러 독립적 Python 상태.

PyInterpreterState *sub = Py_NewInterpreter();

// 새 interpreter에서 코드 실행

Py_EndInterpreter(sub);

문제: 이전엔 서브인터프리터들이 **하나의 GIL 공유**. 병렬 실행 불가. "왜 있는 건지" 의문.

7.2 PEP 684

**Per-Interpreter GIL** (Python 3.12). 각 서브인터프리터가 **자기 GIL**을 가짐.

프로세스

├── Main Interpreter (GIL 1)

├── Sub 1 (GIL 2) ← 독립 병렬 실행

└── Sub 2 (GIL 3) ← 독립 병렬 실행

결과: C API로 서브인터프리터 생성하면 진짜 병렬 Python 코드 실행 가능.

7.3 Python에서 사용

Python 3.13+에서 `interpreters` 모듈 (실험):

interp = interpreters.create()

interp.run("""

print(math.pi)

""")

각 interpreter에서 코드가 진짜 병렬 실행.

7.4 한계

- **공유 객체 제한**: 두 interpreter가 같은 Python 객체를 공유 불가 (채널로 전달).

- **C 확장 호환성**: 많은 기존 확장이 per-interpreter state 지원 안 함.

- **메모리 비용**: 각 interpreter가 자체 상태.

하지만 "CPU-bound Python 병렬"의 새 옵션.

7.5 GIL 없는 프로세스 vs 공유 메모리 interpreter

**multiprocessing**: 프로세스 분리 → 완전 독립, 공유 비싸다.

**Sub-interpreter**: 프로세스 공유, 공유 객체 제한 → 경량 병렬.

중간 지점. 특정 워크로드(plugin system, sandbox)에 유용.

8. Python 3.13 — Free-Threaded Build

8.1 Sam Gross의 실험

2021년 Sam Gross가 **"nogil" 브랜치**를 공개. CPython을 GIL 없이 실행. 성능 하락 최소화.

놀라운 결과였다. 몇 년에 걸친 개선 후 **PEP 703** 수락.

8.2 PEP 703

**Python 3.13 (2024년 10월)**: Free-threaded build 옵션 추가.

./configure --disable-gil

make

./python

이 빌드는:

- **GIL 없음**.

- 여러 스레드가 진짜 병렬 실행.

- 모든 내장 객체는 **thread-safe** (새 lock 필요).

8.3 기술적 도전

**Reference counting**: 원래 GIL이 보호했다. GIL 없으면 race condition.

해결: **biased reference counting**. 각 객체의 "owner" 스레드를 추적. Owner가 수정 시 lock 없이, 다른 스레드가 수정 시 atomic.

**Dict thread-safety**: 모든 dict에 per-bucket lock.

**GC 변경**: stop-the-world를 줄이고 incremental 수준으로.

8.4 성능

초기 결과:

- **Single-threaded**: GIL 없는 버전이 ~5-10% 느림 (atomic 오버헤드).

- **Multi-threaded CPU**: N 스레드에서 선형 확장 (GIL 버전보다 N배).

"Tradeoff". 싱글 스레드 코드는 약간 느리지만 멀티는 대폭 빠름.

8.5 Python 3.13 상태

**실험적**. 기본 빌드가 아님. 사용자가 `--disable-gil`로 직접 빌드 또는 특수 wheel 설치.

**Python 3.14**부터 기본에 포함될 가능성. **Python 3.15**에 안정화.

생태계 호환성이 주요 과제: 수많은 C 확장이 thread-safety 검토 필요.

9. Python 3.13 — Copy-and-Patch JIT

9.1 JIT의 오래된 약속

PyPy가 오래 전부터 JIT으로 Python을 수 배 빠르게. 하지만 복잡하고 호환성 이슈.

CPython은 JIT을 피해왔다. 2024년 바뀌었다.

9.2 PEP 744

**Copy-and-Patch JIT**. Brandt Bucher, Mark Shannon 등이 주도.

특징:

- **템플릿 기반**: 각 opcode에 대해 미리 컴파일된 기계어 스니펫.

- **초고속 컴파일**: 단순히 스니펫을 복사하고 주소를 패치.

- **Tier 2 optimizer**: Tier 1(인터프리터)로 시작, 핫 경로만 JIT.

9.3 아이디어

일반 JIT은 "온디맨드 컴파일러" — 런타임에 LLVM 같은 걸 돌림. **느리다**.

Copy-and-patch: 빌드 시점에 각 opcode의 기계어를 LLVM으로 컴파일해두고, 런타임에는 **메모리 복사 + 주소 수정**만.

// 빌드 시 컴파일된 템플릿:

// (pseudo code)

const char LOAD_FAST_template[] = {

// mov rax, [rdi + <FRAME_OFFSET_PLACEHOLDER>]

0x48, 0x8B, 0x47, 0x00, 0x00, 0x00, 0x00,

// push rax

0x50,

...

};

// 런타임:

memcpy(code_buffer, LOAD_FAST_template, sizeof(LOAD_FAST_template));

patch_offset(code_buffer + 3, actual_offset);

수 μs에 기계어 생성. LLVM JIT는 수 ms.

9.4 Tier 2 Optimizer

**Tier 1**: 일반 인터프리터 (인라인 캐시 포함).

**Tier 2**: 핫 trace를 검출해서 specialized bytecode (superinstruction) 생성.

**Tier 2 JIT**: Tier 2의 superinstruction을 copy-and-patch로 기계어화.

단계적 최적화. 가장 뜨거운 코드만 기계어.

9.5 성능

초기 벤치마크: **추가 5-10% 성능**. PEP 659의 specialization과 결합.

Python 3.13은 JIT이 **실험적**. Python 3.14부터 성숙. **Python 3.15-3.16**에서 주류.

최종 목표: **C 수준의 속도에 점진적 접근**. PyPy 수준(2-10배)까지는 아직 멀지만 방향이 확립됐다.

10. Generator와 Coroutine

10.1 Generator Function

def count_up():

n = 0

while True:

yield n

n += 1

이건 일반 함수가 아니다. `yield`가 있으면 **generator function**. 호출 시 generator object 반환.

gen = count_up()

next(gen) # 0

next(gen) # 1

next(gen) # 2

10.2 Frame Suspension

Generator의 핵심: **frame이 유지된다**.

일반 함수는 리턴 시 frame 해제. Generator는 `yield` 시 frame을 **suspend** — 다음 `next()`까지 보존.

gen = count_up()

Frame 생성, n=0

next(gen) → 루프 진입 → yield 0 → frame suspend

next(gen) → frame resume → n += 1 → yield 1 → suspend

...

C 수준에서 `_PyInterpreterFrame`이 heap에 유지되고 `prev_instr`가 yield 다음 위치를 가리킴.

10.3 Coroutine과 async

async def fetch():

data = await download()

return process(data)

`async def`는 generator의 일반화. **coroutine** 객체 반환. `await`은 yield의 일반화.

내부적으로 같은 frame suspension 메커니즘. `asyncio` 이벤트 루프가 이 coroutine들을 스케줄.

10.4 yield from / await chaining

async def outer():

return await inner()

내부적으로 frame chain. `outer`의 frame이 `inner`의 frame을 기다림. Stack 없이 frame 기반 호출 스택.

10.5 Async 성능

Python의 async는 **C 레벨 frame suspension** 덕분에 매우 빠르다. 스레드보다 가볍다 (고루틴 유사). 하지만 GIL 때문에 CPU 병렬은 여전히 불가.

11. Import System과 Bytecode Caching

11.1 import 동작

CPython:

1. `sys.modules`에서 'math' 확인. 있으면 반환.

2. `sys.meta_path`의 finder들을 순회.

3. Finder가 loader 반환.

4. Loader가 module 생성 + 코드 실행.

5. `sys.modules`에 등록.

11.2 .pyc 캐시

소스 파일:

a.py

캐시:

__pycache__/a.cpython-312.pyc

다음 import 시:

1. `a.py` mtime 확인.

2. `.pyc`의 mtime과 비교.

3. 일치하면 `.pyc` 로드 (파싱 skip → 빠름).

4. 다르면 재컴파일.

11.3 Invalidation 모드

전통: timestamp 기반

python -m py_compile a.py

hash 기반 (3.7+)

python -m py_compile --invalidation-mode=checked_hash a.py

Hash 기반은 reproducible builds에 필요 (Docker, CI).

11.4 Frozen Modules

일부 stdlib 모듈은 **frozen**: 바이트코드가 CPython 바이너리 자체에 포함.

>>> import sys

>>> sys.modules['_frozen_importlib']

Frozen 모듈은 디스크 읽기 불필요 → 매우 빠른 startup. Python 3.11+ 많은 stdlib 모듈이 frozen.

12. Exception Handling

12.1 `try` 블록 바이트코드

Python 3.10까지:

try:

risky()

except ValueError:

handle()

SETUP_FINALLY target

risky()

POP_BLOCK

JUMP_FORWARD

target:

<예외 처리 경로>

**SETUP_FINALLY**: "이 이후 예외가 나면 target으로 점프".

12.2 Python 3.11: Zero-Cost Exceptions

3.11은 **예외 처리가 공짜**다. try 블록 진입 시 bytecode 오버헤드 없음.

대신 exception table을 사용:

exception_table:

[bytecode range] → [handler location, stack_depth]

예외 발생 시 이 테이블을 조회 → 해당 범위의 핸들러 찾기.

"try 블록이 많아도 성능 손실 없음". Rust의 접근과 유사 (DWARF-based unwinding).

12.3 Raise와 Unwinding

`raise` 시:

1. Exception 객체 생성.

2. Frame stack 순회하며 handler 찾기.

3. 없으면 프로그램 종료.

4. 있으면 stack unwind 후 handler 실행.

각 frame의 exception table 조회. unwind마다 frame의 로컬 정리 (finalizer 호출).

13. 튜닝과 최적화

13.1 무엇이 느린가 찾기

**cProfile**:

cProfile.run("myfunc()")

43 function calls in 0.005 seconds

ncalls tottime percall cumtime percall filename:lineno(function)

1 0.000 0.000 0.005 0.005 <string>:1(<module>)

10 0.001 0.000 0.005 0.000 a.py:5(work)

30 0.004 0.000 0.004 0.000 a.py:2(helper)

**line_profiler**:

@profile

def myfunc():

줄 단위 시간 측정

**py-spy**: 샘플링 프로파일러. 실행 중인 프로세스에 attach 가능.

13.2 일반 팁

**Built-in 사용**:

sum(xs) # 빠름

vs

total = 0

for x in xs: total += x # 느림

**Local var > Global**:

def f():

sqrt = math.sqrt # local로

for x in data:

y = sqrt(x) # LOAD_FAST (빠름)

LOAD_FAST는 LOAD_GLOBAL보다 빠름.

**List comprehension > for loop**:

[x*2 for x in data] # 빠름 (특수 최적화)

**Generator에서 list()**: 불필요한 list 생성 피하기.

13.3 Cython / PyPy / C Extension

**Cython**: Python 문법 + type annotation → C 컴파일 → 10-100x.

fib.pyx

def fib(int n):

cdef int a = 0, b = 1

for _ in range(n):

a, b = b, a + b

return a

**PyPy**: CPython 대체. 자체 JIT. 호환성 이슈.

**C 확장 직접 작성**: 최고 성능, 개발 시간.

**FFI (cffi, ctypes)**: 이미 있는 C 라이브러리 wrap.

13.4 New Alternatives

**Mojo** (Modular): Python 상위 집합 + 타입 + AI 최적화.

**Codon**: LLVM 기반 Python 컴파일러.

**Nuitka**: Python → C++.

14. 내부 탐험 도구

14.1 dis

dis.dis(my_function)

dis.show_code(my_function.__code__)

14.2 gc

gc.get_objects() # 모든 객체

gc.collect() # 수동 GC

gc.get_threshold() # generational 임계

gc.set_debug(gc.DEBUG_STATS)

14.3 sys

sys.getsizeof(obj) # 객체 크기 (바이트)

sys.getrefcount(obj) # 참조 카운트

sys.settrace(tracer) # 모든 함수 호출 훅

sys.getframeinfo(frame)

14.4 tracemalloc

tracemalloc.start()

... code ...

snapshot = tracemalloc.take_snapshot()

top_stats = snapshot.statistics('lineno')

for stat in top_stats[:10]:

print(stat)

메모리 할당 추적. "어디서 메모리를 많이 쓰나" 답변.

14.5 CPython 소스 읽기

**가장 배우는 방법**. GitHub: `python/cpython`.

주요 파일:

- `Python/ceval.c`: 인터프리터 루프.

- `Objects/`: 모든 내장 타입 (`longobject.c`, `listobject.c` 등).

- `Python/compile.c`: AST → bytecode.

- `Include/internal/`: 내부 헤더.

15. 역사적 변화 타임라인

15.1 주요 변화

- **Python 1.0** (1994): 첫 안정화.

- **Python 2.0** (2000): 리스트 comprehension, GC.

- **Python 3.0** (2008): 대대적 정리. 호환성 포기.

- **Python 3.6** (2016): f-string, type hint 기본.

- **Python 3.8** (2019): Walrus operator, Positional-only params.

- **Python 3.9** (2020): PEG parser.

- **Python 3.10** (2021): Structural pattern matching, Better errors.

- **Python 3.11** (2022): **Specializing adaptive interpreter** (PEP 659). 25% 빠름.

- **Python 3.12** (2023): Per-interpreter GIL, type param syntax.

- **Python 3.13** (2024): **Free-threaded build**, **JIT experimental**.

- **Python 3.14** (2025 예정): JIT 확장, free-threaded 성숙.

15.2 Performance 진화

Python 3.10: baseline

Python 3.11: 1.25x 빠름

Python 3.12: 1.3x

Python 3.13: ~1.4x (JIT 포함 시)

Python 3.15 예상: 2-3x

Faster CPython 프로젝트 목표: **Python 3.10의 5배**. 아직 갈 길이 있지만 방향 확립.

16. 학습 리소스

**책**:

- "CPython Internals" — Anthony Shaw. 유일한 종합서.

- "Python Internals for Developers" — Obi Ike-Nwosu.

**온라인**:

- "Faster CPython" 프로젝트: GitHub discussions.

- Łukasz Langa의 블로그 (Python core dev).

- Brett Cannon의 "How import works" 시리즈.

**영상**:

- "CPython from the Inside Out" — Philip Guo.

- PyCon, EuroPython의 CPython internals 토크.

- Guido van Rossum, Mark Shannon, Brandt Bucher의 발표들.

**코드**:

- `python/cpython` GitHub.

- PEP 659, 703, 744 문서.

**대안 구현**:

- **PyPy**: JIT 기반.

- **MicroPython**: 임베디드.

- **RustPython**: Rust로 재구현.

- **Pyston**: LLVM 기반 (Dropbox 출신).

17. 요약 — 한 장 정리

┌─────────────────────────────────────────────────────┐

│ CPython Internals Cheat Sheet │

├─────────────────────────────────────────────────────┤

│ 컴파일 파이프라인: │

│ Source → Lexer → Parser (PEG) │

│ → AST → Compiler → Bytecode → Code Object │

│ → .pyc 캐시 │

│ │

│ Bytecode: │

│ Stack-based VM │

│ ~200 opcodes │

│ LOAD_FAST, BINARY_OP, CALL, RETURN_VALUE │

│ dis 모듈로 확인 │

│ │

│ ceval.c: │

│ _PyEval_EvalFrameDefault (메인 루프) │

│ 거대한 switch 또는 computed goto │

│ 각 opcode = C 코드 handler │

│ │

│ Frame: │

│ _PyInterpreterFrame │

│ 로컬 + 스택 (localsplus 배열) │

│ Python 3.11+: C stack 할당 (빠름) │

│ │

│ PEP 659 (3.11+): │

│ Specializing adaptive interpreter │

│ Inline cache (바이트코드 옆) │

│ Opcode를 specialized version으로 in-place 교체 │

│ LOAD_ATTR_INSTANCE_VALUE │

│ BINARY_OP_ADD_INT │

│ De-optimize on fail │

│ +25% 성능 │

│ │

│ PEP 684 (3.12): │

│ Per-interpreter GIL │

│ 서브인터프리터 진짜 병렬 │

│ │

│ PEP 703 (3.13): │

│ Free-threaded build │

│ GIL 제거 (실험) │

│ Biased reference counting │

│ │

│ PEP 744 (3.13): │

│ Copy-and-patch JIT │

│ 템플릿 기반 초고속 컴파일 │

│ Tier 2 optimizer │

│ 실험적 │

│ │

│ Generator / async: │

│ Frame suspension │

│ yield / await = frame 보존 │

│ │

│ Exception (3.11+): │

│ Zero-cost exception handling │

│ Exception table │

│ │

│ 성능 툴: │

│ cProfile, line_profiler, py-spy │

│ tracemalloc, gc module │

│ │

│ 대안 구현: │

│ PyPy (JIT) │

│ Cython (C 컴파일) │

│ MicroPython, RustPython │

│ Mojo, Codon (2024+) │

└─────────────────────────────────────────────────────┘

18. 퀴즈

**A.** 부분적으로 맞지만 오해를 일으킨다. Python 소스는 **바이트코드로 컴파일**된다 — `.pyc` 파일이 이 결과. 실행 시점에는 기계어가 아니라 **바이트코드 VM**(`ceval.c`의 거대한 dispatch loop)이 해석한다. 즉 "컴파일되는 것 맞지만 타겟이 기계어가 아니라 VM 명령어"다. Java와 C#도 같은 접근 (JVM, CLR). 차이는 Java/C#이 JIT으로 기계어화하는 반면 CPython은 오랫동안 pure interpreter였다. Python 3.13부터 copy-and-patch JIT이 실험적으로 추가 → "pure interpreter" 시대가 끝나가고 있다.

**A.** **분기 예측 정확도**. Switch 기반 dispatch는 모든 opcode가 하나의 jump 위치에서 분기 → CPU의 branch predictor가 "다음 opcode"를 예측하기 어려움(pattern이 opcode 시퀀스에 달림). Computed GOTO는 **각 opcode handler 끝에서 직접 다음 opcode로 점프**하는 코드를 두어서, 각 jump location마다 **독립된 branch history**를 가진다. 예: `LOAD_FAST` 뒤에 자주 `LOAD_FAST`가 오면 그 jump만 그 패턴을 학습. 결과: 10-15% 빠른 인터프리터. GCC/Clang의 `&&label` 확장 필요하지만 이것이 현대 인터프리터(Python, Ruby, Lua, V8)의 표준.

**A.** **Inline cache 기반 런타임 opcode 교체**. 각 opcode 옆에 cache slot이 있어서 첫 실행 시 프로파일링(어떤 타입? 어떤 offset?) 후 **opcode 자체를 specialized version으로 in-place 교체**. 예: `LOAD_ATTR` → 첫 실행 시 `obj`가 특정 type이고 `name`이 그 type의 `__dict__`에 있음을 확인 → opcode가 `LOAD_ATTR_INSTANCE_VALUE`로 바뀜. 다음 실행: type version tag 확인(빠름) → 일치 시 dict 검색 건너뛰고 직접 offset 접근 → 기존 대비 5-10배 빠름. 가정이 틀리면 de-optimize → 원래 opcode로 복귀. V8이나 HotSpot의 inline cache를 "JIT 없이 인터프리터에만" 적용한 것. Python 3.11에서 25%+ 성능 향상의 주역.

**A.** **Reference counting의 atomic 비용**. CPython은 모든 객체에 refcount가 있고, 참조할 때마다 +1, 해제할 때마다 -1 한다. GIL이 있는 buildings에서는 단순 정수 연산(수 ns). GIL이 없으면 refcount가 여러 스레드에서 동시 수정될 수 있으므로 **atomic operation**이 필요(수십 ns). 모든 객체 접근에 이 오버헤드가 붙어서 single-threaded 벤치마크에서 5-10% 느려진다. Sam Gross의 해결책: **biased reference counting** — "owner thread"는 atomic 없이 증감, 다른 스레드만 atomic. 평균적으로 여전히 약간 느리지만 multi-threaded에서는 선형 확장. Trade-off: "single-threaded 5% 손해 vs multi-threaded N배 이득".

**A.** **컴파일 시점과 런타임 분리**. 일반 JIT은 런타임에 LLVM 같은 컴파일러를 돌려 기계어를 생성 → 컴파일 시간 수 ms. 대부분 프로그램이 런타임에 수만 함수를 본다면 JIT 비용이 누적. Copy-and-Patch는 **빌드 시점에 LLVM으로 각 opcode의 기계어 템플릿을 컴파일**해두고, 런타임에는 **메모리에 템플릿을 복사하고 주소/상수만 패치**. 수 μs에 기계어 생성 — LLVM의 1000배 빠름. 품질은 LLVM보다 덜 공격적이지만 Python의 특성상 "JIT의 75%를 25%의 노력으로" 얻는다. Python 3.13의 Tier 2 optimizer에서 실험적으로 사용, 2025-2026년 성숙 예정. 작은 혁신(템플릿 아이디어)이 기존 JIT 인프라를 뒤흔드는 사례.

**A.** **Frame의 생명주기**. 일반 함수는 호출 시 frame 생성, 리턴 시 해제. Generator는 `yield` 시 **frame을 suspend** — frame을 heap에 유지하고 `prev_instr`(현재 명령어 포인터)를 yield 다음 위치로 설정. `next()` 호출 시 frame을 resume → 마지막 위치부터 계속. 로컬 변수, eval stack, 모든 상태가 frame에 보존되므로 "함수가 중간에서 재개"되는 것처럼 동작. `async def`는 동일 메커니즘의 일반화 — coroutine은 generator의 확장. 이 덕분에 Python async는 스레드 없이 가볍게 동작하고(고루틴과 유사), `asyncio` 이벤트 루프가 coroutine의 frame을 스케줄한다. 모든 것이 결국 "frame 보존" 트릭 하나.

**A.** **`try` 블록 진입 시의 bytecode 오버헤드**. Python 3.10까지는 `try` 진입 시 `SETUP_FINALLY` opcode가 실행되어 "예외 발생 시 어디로 점프할지"를 설정. 예외가 없어도 이 오버헤드가 매번 발생. 3.11+는 **exception table**을 코드 오브젝트에 저장 — "bytecode 범위 X~Y에서 예외 발생 시 Z로"를 컴파일 타임에 정적으로 기록. 런타임에 `try` 진입은 **아무 일도 안 함** (bytecode 실행 안 함). 예외가 실제로 발생하면 그때 테이블 조회. Rust의 DWARF 기반 zero-cost exceptions과 같은 접근. 결과: try/except가 많은 코드에서 성능 손실 없음. "예외는 정상 경로를 느리게 하지 않는다"는 원칙 준수.

이 글이 도움이 됐다면 다음 포스트도 확인해 보세요:

- "Python GIL과 CPython 내부" — 같은 프로젝트의 병렬성 이슈.

- "JIT Compilation V8 & JVM" — 유사한 기법의 다른 언어.

- "Rust Tokio Async Runtime" — zero-cost async의 극단적 접근.

- "Diffusion Models" — Python이 AI 워크로드를 돌리는 배경.

현재 단락 (1/691)

- **CPython**은 "Python 소스 → AST → 바이트코드 → `ceval.c` 인터프리터" 파이프라인으로 실행된다. "해석 언어"라지만 실제로는 **컴파일 후 VM ...

작성 글자: 0원문 글자: 21,403작성 단락: 0/691