- Published on
CPython Bytecode Interpreter Deep Dive — ceval.c、Specializing Adaptive Interpreter、PEP 659、Copy-and-Patch JIT 完全解説 (2025)
- Authors

- Name
- Youngju Kim
- @fjvbn20031
TL;DR
- CPython は「Python ソース → AST → Bytecode →
ceval.cインタプリタ」のパイプラインで動作する。「インタプリタ言語」だが実体は コンパイル後に VM 実行。 - Bytecode: スタック型 VM 命令。
LOAD_FAST、BINARY_OP、CALLなど。.pycに保存。 - ceval.c: Python の心臓。巨大な switch または computed-goto ループ。すべての Python コードがここを通る。
- Python 3.11+ PEP 659: Specializing Adaptive Interpreter。ホットパスを実行時に特殊化 opcode に置換 → 25%+ 高速化。
- Inline cache: 各 opcode 直後に cache slot。初回実行でプロファイル → 特殊化 → 以後高速。
- Python 3.12 PEP 684: Per-interpreter GIL。サブインタプリタごとに独立 GIL。
- Python 3.13 PEP 703: free-threading build。GIL 除去 (実験的)。
- Python 3.13 PEP 744: Copy-and-Patch JIT。Tier 2 optimizer、テンプレート方式の超高速コード生成。
- Frame object: 各関数呼び出しの実行コンテキスト。locals、stack、instruction pointer、例外状態。
- dis モジュール:
dis.dis(f)で Bytecode を確認。
1. Python が「遅い」理由
1.1 動的型付けのコスト
x + y
C なら int + int は CPU 命令 1 個 (add)。Python は:
x、yの型確認。x.__add__のルックアップ。- なければ
y.__radd__。 - メソッド呼び出し。
- 結果オブジェクト生成 (ヒープ確保)。
- Reference count 調整。
C 命令数十個ぶん、100 倍以上遅い。
1.2 Boxing
Python の値はすべて オブジェクト。int 1 つも PyLongObject:
typedef struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
} PyLongObject;
32 バイト以上の構造体。「1」もこのサイズ。
1.3 Reference counting
Py_INCREF(obj);
Py_DECREF(obj);
全オブジェクト操作にこのコスト。マルチスレッドでは atomic が必要 → より遅い。GIL が存在する理由の 1 つ。
1.4 「Python は永遠に遅いのか?」
否。2021 年以降 CPython は 性能革命 中:
- Python 3.11 (2022 年 10 月): 平均 25% 高速。
- Python 3.12 (2023): +5%。
- Python 3.13 (2024): free-threading + JIT (実験)。
- Python 3.14 (2025 予定): より積極的な最適化。
「Faster CPython」プロジェクト (Guido が Microsoft 参画後に開始) の目標: Python 3.10 比 5 倍。
2. コンパイルパイプライン
2.1 ステージ
Python source (hello.py)
|
Lexer (tokenizer)
|
Parser
|
AST
|
Compiler (ast -> bytecode)
|
Bytecode (code object)
|
ceval.c interpreter
Python も「コンパイル」する。ターゲットが機械語ではなく VM 命令、という意味。
2.2 Lexer と Parser
Python 3.9+ は PEG parser (Pegen)。以前は LL(1)。PEG はより柔軟。
2.3 AST
import 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 モジュールで AST を変換可能 → macro に近い操作。
2.4 Code object
def add(a, b):
return a + b
print(add.__code__.co_code)
print(add.__code__.co_consts)
print(add.__code__.co_varnames)
PyCodeObject の主要フィールド:
co_code: Bytecode。co_consts: 定数プール。co_names: グローバル/属性名。co_varnames: ローカル名。co_flags: async、generator などのフラグ。co_lnotab: Bytecode offset → ソース行番号。co_stacksize: 最大スタック深度。
2.5 .pyc ファイル
Code object は marshal 形式で .pyc に保存。__pycache__/ 配下。
magic number (バージョン)
source mtime または hash
source size
marshaled code object
再実行時、Python は .py と .pyc を比較して未変更なら .pyc を読み込み (parse skip)。3.7+ は SOURCE_DATE_EPOCH 対応の reproducible .pyc をサポート。
3. Bytecode 解剖
3.1 dis モジュール
import dis
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
3.2 Opcode
Python 3.12 で約 200 個:
スタック: LOAD_CONST、LOAD_FAST、LOAD_GLOBAL、LOAD_ATTR、STORE_FAST、POP_TOP。
算術: BINARY_OP、COMPARE_OP。
制御: JUMP_FORWARD、POP_JUMP_IF_FALSE、FOR_ITER。
呼び出し: CALL、RETURN_VALUE、MAKE_FUNCTION。
構築: BUILD_LIST、BUILD_TUPLE、BUILD_DICT。
3.3 スタック型
Python VM は スタック型。レジスタ無し。
a + b:
LOAD_FAST 0 # stack: [a]
LOAD_FAST 1 # stack: [a, b]
BINARY_OP 0 # a, b pop -> a+b push
RETURN_VALUE
利点: 命令が単純、コンパイルが容易。欠点: 命令数が多い、JIT に不利。JVM、CLR、Lua も同様。Dalvik はレジスタ型。
3.4 Python 3.11 の変更
従来は BINARY_ADD、BINARY_SUB など個別 opcode。3.11+ で BINARY_OP に統合:
LOAD_FAST 0
LOAD_FAST 1
BINARY_OP 0 # 0=+, 5=*, ...
統合理由: PEP 659 の specialization を容易にするため。すべての二項演算が単一の adaptive opcode を通る。
3.5 より複雑な例
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
再帰呼び出し、条件分岐、算術の組み合わせが dis.dis(fib) で見える。
4. ceval.c — Python の心臓
4.1 ファイル概要
Python/ceval.c は CPython で最も重要なファイル。約 8,000 行 (3.12)。
主要関数:
_PyEval_EvalFrameDefault(): メインインタプリタループ。PyEval_EvalCode(): トップレベル評価。
4.2 Frame 実行
関数呼び出し時:
- 新規 frame object 生成。
- ローカル変数初期化。
_PyEval_EvalFrameDefault(frame)呼び出し。- ループ実行。
- return 時に 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];
} _PyInterpreterFrame;
localsplus が鍵。ローカル変数と eval stack を単一配列に統合 (性能のため)。
4.3 Main loop
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 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: {
return *(--stack_pointer);
}
}
}
}
4.4 Computed GOTO
switch は遅い場合がある: jump site が 1 箇所で分岐予測が効きにくい。GCC/Clang は computed goto 拡張をサポート:
#define DISPATCH() goto *opcode_targets[opcode];
各ハンドラ末尾で直接ジャンプ。CPU の branch predictor が opcode ごとの 独立 history を持てる → switch より 10-15% 高速。USE_COMPUTED_GOTOS マクロで決まる (GCC/Clang で既定有効)。
4.5 Dispatch overhead
各 opcode で fetch-decode-execute-branch に数 ns。100 万命令で数 ms の純粋オーバーヘッド。C は同じ作業を数百 ns。これが Python の遅さの大部分。
5. Frame と呼び出しスタック
5.1 Frame の役割
各関数呼び出しが 1 frame。
5.2 Python 3.11 の改善
- 以前: 各 frame がヒープオブジェクト (malloc)。
- 3.11+: frame を C stack に確保。必要時のみヒープ。
結果: 関数呼び出し 50% 高速化。
5.3 Traceback
例外発生時、traceback は frame stack を辿る。
5.4 sys._getframe
import sys
frame = sys._getframe()
print(frame.f_code.co_name)
print(frame.f_locals)
print(frame.f_back)
デバッガ、プロファイラ、logging が利用。
5.5 f-string と Frame
f"{x}" はコンパイル時に解析されるが、値は 実行時 frame のローカル から取得。
6. Specializing Adaptive Interpreter (PEP 659)
Python 3.11 の最大のイノベーション。
6.1 問題
LOAD_ATTR で obj.name を実行するとき:
type(obj)の MRO を走査。nameを descriptor として検索。- descriptor protocol 適用。
- 見つからなければ
obj.__dict__参照。 - 最後に
__getattr__。
複雑。しかし実際には obj は大抵同じ型、name は __dict__ の単純キー。キャッシュ可能。
6.2 Inline cache
Inline cache は各 opcode のそばに cache slot を置く技法。V8、HotSpot が採用。CPython 3.11+ で adaptive opcodes 導入:
LOAD_ATTR 0 <- 汎用 opcode
初回:
- プロファイル: 型と offset を記録
- opcode を LOAD_ATTR_INSTANCE_VALUE に置換 (内部のみ)
以降:
- specialized 版を使用
- 型確認 (想定と同じか?)
- 一致: 直接 offset アクセス (非常に高速)
- 不一致: 元の LOAD_ATTR にフォールバック
6.3 Inline cache 構造
[LOAD_ATTR opcode (2 バイト)] [cache slot (数バイト)]
cache slot の中身: 想定型の version tag、dict offset または descriptor キャッシュ、統計 (hit/miss)。これがまさに inline の理由 — Bytecode ストリーム内に cache 本体が入る。
6.4 Specialization バリアント
LOAD_ATTR:
LOAD_ATTR_INSTANCE_VALUELOAD_ATTR_WITH_HINTLOAD_ATTR_SLOTLOAD_ATTR_MODULELOAD_ATTR_CLASSLOAD_ATTR_METHOD_WITH_VALUESLOAD_ATTR_PROPERTY
各々が独立した 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
add(1, 2) # -> BINARY_OP_ADD_INT
add(1.0, 2.0) # int 前提失敗 -> BINARY_OP に戻る
失敗回数が閾値を超えれば de-optimize。再特殊化も可能。
6.7 性能結果
- pyperformance ベンチマーク: 平均 25%+ 高速。
- 一部ベンチは 60% 超。
- Django テンプレートレンダリング: 15% 高速。
ユーザーはコード変更不要。
6.8 意義
PEP 659 は JIT 無しで 性能向上を実証した。「Python を速くするには JIT が必須」という通念を覆す。単純なインタプリタ最適化だけで大きな利得。JIT が不要という意味ではないが、JIT 無しでも伸びしろがある。
7. Python 3.12 — Per-interpreter GIL
7.1 サブインタプリタ
CPython には古い機能がある: サブインタプリタ。1 プロセス内に独立した Python 状態を複数保持。
ただし以前は全サブインタプリタが 単一 GIL を共有 → 並列実行不可。
7.2 PEP 684
Per-interpreter GIL (Python 3.12)。各サブインタプリタが独自 GIL を持つ。
Process
├── Main interpreter (GIL 1)
├── Sub 1 (GIL 2) <- 真の並列実行
└── Sub 2 (GIL 3) <- 真の並列実行
7.3 Python からの利用
Python 3.13+ の実験的 interpreters モジュール:
import interpreters
interp = interpreters.create()
interp.run("""
import math
print(math.pi)
""")
各 interpreter で真の並列実行。
7.4 制約
- 共有オブジェクト制限: 2 つの interpreter が同じ Python オブジェクトを共有不可 (channel 経由)。
- C 拡張互換性: 多くの既存拡張が per-interpreter state 非対応。
- メモリコスト: 各 interpreter が独自状態。
multiprocessing と thread の中間。plugin system、sandbox に有用。
8. Python 3.13 — Free-threading ビルド
8.1 Sam Gross の実験
2021 年、Sam Gross が "nogil" ブランチ を公開。GIL 無し CPython を最小性能損失で実現。数年の改善を経て PEP 703 承認。
8.2 PEP 703
Python 3.13 (2024 年 10 月): free-threading ビルドオプション追加。
./configure --disable-gil
make
./python
- GIL 無し。
- 複数 thread が真の並列実行。
- 組み込みオブジェクトすべて thread-safe 化 (新規 lock)。
8.3 技術的課題
Reference counting: 元々 GIL が守っていた。GIL が無いと race condition。
解: biased reference counting。各オブジェクトの owner thread を追跡。owner は lock 無しで更新、他 thread は 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 threads で線形スケール。
トレードオフ: single-thread で少し遅いが multi-thread で大幅高速化。
8.5 状態
実験的。既定ビルドではない。--disable-gil で自前ビルドまたは特別 wheel。3.14 で既定入り可能性、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 でコンパイル済みにしておき、実行時は メモリコピー + アドレス修正のみ。
// ビルド時にコンパイル済みテンプレート (疑似):
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 なので約 1000 倍高速。
9.4 Tier 2 optimizer
- Tier 1: 通常インタプリタ (inline cache 込み)。
- Tier 2: ホット trace を検出し、specialized Bytecode (superinstruction) 生成。
- Tier 2 JIT: Tier 2 superinstruction を copy-and-patch で機械語化。
最もホットなコードだけが native に。
9.5 性能
初期ベンチマーク: +5-10% (PEP 659 の specialization と組み合わせ)。Python 3.13 では JIT は 実験的。3.14 で成熟、3.15-3.16 で主流化見込み。最終目標: C レベル速度への段階的接近。
10. Generator と Coroutine
10.1 Generator function
def count_up():
n = 0
while True:
yield n
n += 1
yield があれば generator function。呼び出すと generator object を返す。
10.2 Frame suspension
Generator の核心: frame が維持される。通常関数は return で frame 解放だが、generator は yield 時に frame を suspend — 次 next() まで保存。
C レベルで _PyInterpreterFrame がヒープに残り、prev_instr が yield の次を指す。
10.3 Coroutine と async
async def fetch():
data = await download()
return process(data)
async def は generator の一般化。await は yield の一般化。内部は同じ frame suspension。asyncio イベントループがこれら coroutine をスケジュール。
10.4 Chain
async def outer():
return await inner()
frame chain: outer frame が inner frame を待つ。
10.5 Async 性能
Python async は C レベル frame suspension のおかげで非常に軽量。thread より軽い (goroutine 類似)。ただし GIL のため CPU 並列は不可。
11. Import システムと Bytecode キャッシュ
11.1 import の動作
import math
sys.modulesを確認。sys.meta_pathの finder を走査。- finder が loader を返す。
- loader がモジュール生成 + コード実行。
sys.modulesに登録。
11.2 .pyc キャッシュ
a.py -> __pycache__/a.cpython-312.pyc
mtime または hash を比較して未変更なら .pyc を読む。
11.3 Invalidation モード
python -m py_compile a.py
python -m py_compile --invalidation-mode=checked_hash a.py
hash ベースは reproducible builds に必要 (Docker、CI)。
11.4 Frozen modules
一部 stdlib は frozen (Bytecode が CPython バイナリに埋め込み):
>>> import sys
>>> sys.modules['_frozen_importlib']
<module '_frozen_importlib' (frozen)>
ディスク読み込み不要 → 高速起動。Python 3.11+ で多くの stdlib が frozen。
12. 例外処理
12.1 try ブロック Bytecode (3.10 まで)
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]
例外発生時にこのテーブルを参照。Rust の DWARF-based unwinding に類似。
12.3 Raise と unwinding
raise 時:
- 例外オブジェクト生成。
- frame stack を辿り handler を探索。
- 無ければ終了。
- 有れば unwind 後 handler 実行。
13. チューニングと最適化
13.1 遅い箇所の特定
cProfile:
import cProfile
cProfile.run("myfunc()")
line_profiler、py-spy (サンプリング、実行中プロセスに attach)。
13.2 一般的な Tips
組み込み使用:
sum(xs) # 高速
Local > Global:
def f():
sqrt = math.sqrt # local に
for x in data:
y = sqrt(x)
LOAD_FAST は LOAD_GLOBAL より高速。
List comprehension: 特殊最適化。不要な list() は避ける。
13.3 Cython / PyPy / C 拡張
- Cython: Python 構文 + 型注釈 -> C コンパイル -> 10-100x。
- PyPy: CPython 代替、独自 JIT、互換性問題。
- C 拡張自作: 最高性能、開発コスト。
- FFI (cffi、ctypes): 既存 C ライブラリのラップ。
13.4 新しい選択肢
Mojo (Modular): Python 上位集合 + 型 + AI 最適化。 Codon: LLVM ベース Python コンパイラ。 Nuitka: Python -> C++。
14. 内部探検ツール
14.1 dis
import dis
dis.dis(my_function)
dis.show_code(my_function.__code__)
14.2 gc
import gc
gc.get_objects()
gc.collect()
gc.get_threshold()
14.3 sys
sys.getsizeof(obj)
sys.getrefcount(obj)
sys.settrace(tracer)
14.4 tracemalloc
import tracemalloc
tracemalloc.start()
snapshot = tracemalloc.take_snapshot()
14.5 CPython ソース読み
最良の学習法。GitHub: python/cpython。
Python/ceval.c: インタプリタループ。Objects/: 組み込み型。Python/compile.c: AST -> Bytecode。Include/internal/: 内部ヘッダ。
15. 歴史的変遷
- Python 3.6 (2016): f-string。
- Python 3.9 (2020): PEG parser。
- Python 3.10 (2021): pattern matching。
- Python 3.11 (2022): Specializing Adaptive Interpreter (PEP 659)。25% 高速。
- Python 3.12 (2023): per-interpreter GIL。
- Python 3.13 (2024): free-threading build、JIT 実験。
- Python 3.14 (2025): JIT 拡張、free-threading 成熟。
3.10 比: 3.11 ~1.25x、3.12 ~1.3x、3.13 ~1.4x (JIT 込み)、3.15 目標 2-3x。Faster CPython 最終目標は 3.10 の 5 倍。
16. 学習リソース
書籍: "CPython Internals" (Anthony Shaw)、"Python Internals for Developers" (Obi Ike-Nwosu)。
オンライン: Faster CPython GitHub discussions、Łukasz Langa ブログ、Brett Cannon の import シリーズ。
動画: "CPython from the Inside Out" (Philip Guo)、PyCon / EuroPython の internals セッション。
コード: python/cpython、PEP 659、703、744。
代替実装: PyPy (JIT)、MicroPython、RustPython、Pyston。
17. クイズ
Q1. Python を「インタプリタ言語」と呼ぶのは正確か?
A. 部分的に正しいが誤解を招く。Python ソースは Bytecode にコンパイル される (.pyc がその結果)。実行時には機械語ではなく Bytecode VM (ceval.c の巨大 dispatch loop) が解釈する。Java や C# も同様 (JVM、CLR)。違いは Java/C# が JIT で機械語化するのに対し、CPython は長らく pure interpreter だった点。Python 3.13 から Copy-and-Patch JIT が実験的に加わり、pure interpreter 時代は終わりつつある。
Q2. Computed GOTO が switch より速い理由は?
A. 分岐予測精度。switch ベース dispatch は全 opcode が 1 箇所の jump site で分岐するため、CPU の branch predictor が次 opcode を予測しにくい (パターンが opcode 列に依存)。Computed GOTO は 各 opcode handler 末尾で直接次 opcode にジャンプ するので、各 jump location が 独立した branch history を持てる。例: LOAD_FAST の後によく LOAD_FAST が続くならその site がそのパターンを学習。結果としてインタプリタが 10-15% 高速化。GCC/Clang の &&label 拡張が必要。現代インタプリタ (Python、Ruby、Lua、V8) の標準。
Q3. PEP 659 Specializing Adaptive Interpreter の仕組みは?
A. Inline cache ベースの実行時 opcode 書き換え。各 opcode 近傍に cache slot があり、初回実行時にプロファイリング (どの型? どの offset?) したうえで opcode 自体を specialized 版に in-place 置換。例: LOAD_ATTR → 初回で obj が特定型で name がその型の __dict__ にあると確認 → opcode を LOAD_ATTR_INSTANCE_VALUE に。次回以降は type version tag を確認 (高速) → 一致なら dict 検索を飛ばして直接 offset アクセス → 従来比 5-10 倍。前提が崩れれば de-optimize。V8 や HotSpot の inline cache を「JIT 無しでインタプリタだけに」適用したもの。Python 3.11 で 25%+ 高速化の主因。
Q4. Python 3.13 free-threading ビルドが single-threaded コードを遅くする理由は?
A. Reference counting の atomic コスト。CPython は全オブジェクトに refcount を持ち、参照のたびに +1、解放で -1 する。GIL ありビルドでは単純な整数演算 (数 ns)。GIL 無しでは refcount が複数 thread から同時更新され得るため atomic 操作 が必要 (数十 ns)。全オブジェクトアクセスにこのオーバーヘッドが乗るため single-thread ベンチで 5-10% 遅くなる。Sam Gross の解: biased reference counting — owner thread は atomic 無しで増減、他 thread のみ atomic。平均で少し遅いが multi-thread では線形スケール。トレードオフ: 「single-thread 5% 損 vs multi-thread N 倍得」。
Q5. Copy-and-Patch JIT が既存 JIT (LLVM 等) より速い理由は?
A. コンパイル時点と実行時の分離。一般的 JIT は実行時に LLVM 等を走らせて機械語生成 → 数 ms/関数。プログラムが数万関数見るなら累積が大きい。Copy-and-Patch は ビルド時に LLVM で各 opcode の機械語テンプレートをコンパイル しておき、実行時は メモリにテンプレートをコピーしアドレス/定数だけパッチ。数 μs で機械語生成 — LLVM の 1000 倍高速。品質は LLVM より劣るが、Python の特性上「JIT の 75% を 25% の労力で」得られる。Python 3.13 の Tier 2 optimizer で実験採用。小さなイノベーション (テンプレートのアイデア) が既存 JIT インフラを揺さぶる好例。
Q6. Generator と通常関数の実装の違いは?
A. Frame の生命周期。通常関数は呼び出しで frame 生成、return で解放。Generator は yield で frame を suspend — ヒープに残し、prev_instr (現在命令ポインタ) を yield の次へ設定。next() で frame を resume → 最後の位置から再開。ローカル変数、eval stack、全状態が frame に保存されるため「関数が途中から再開する」ように動く。async def は同じメカニズムの一般化 — coroutine は generator の拡張。これにより Python async は thread 無しで軽量 (goroutine 類似) に動作し、asyncio イベントループが coroutine の frame をスケジュールする。すべては結局「frame 保存」という 1 つのトリック。
Q7. Python 3.11 の「zero-cost exceptions」は何を除去したか?
A. try ブロック突入時の Bytecode オーバーヘッド。3.10 まで try 入りで SETUP_FINALLY opcode が毎回実行されていた (例外が出なくてもコストを払う)。3.11+ は exception table を code object に保存 — 「bytecode 範囲 X..Y で例外発生時は Z へ」をコンパイル時に静的記録。実行時の try 突入は 何もしない (Bytecode 実行無し)。実際に例外が発生した場合にのみテーブルを参照。Rust の DWARF ベース zero-cost exceptions と同じ方針。try/except が多くても性能損失なし。「例外は正常経路を遅くしない」原則に従う。
関連記事:
- "Python GIL と CPython 内部" — 同プロジェクトの並行性問題。
- "JIT Compilation: V8 and JVM" — 他言語での類似技法。
- "Rust Tokio Async Runtime" — zero-cost async の極致。
- "Diffusion Models" — Python が AI ワークロードを動かす背景。