3.3 Bytecode 與虛擬機
3.3 Bytecode 與虛擬機
Python 先把原始碼編譯成 bytecode,再由虛擬機執行。理解這個過程有助於優化程式碼效能。
先備知識
本章目標
學完本章後,你將能夠:
- 理解 Python 的編譯流程
- 使用
dis模組分析 bytecode - 從 bytecode 角度理解效能差異
- 了解 Python 3.11+ 的效能優化
【原理層】Python 的執行流程
編譯與執行
1原始碼 (.py)
2 │
3 ▼
4┌─────────────┐
5│ 詞法分析 │ ← 將原始碼分解成 tokens
6│ (Lexer) │
7└─────────────┘
8 │
9 ▼
10┌─────────────┐
11│ 語法分析 │ ← 建立抽象語法樹 (AST)
12│ (Parser) │
13└─────────────┘
14 │
15 ▼
16┌─────────────┐
17│ 編譯器 │ ← 將 AST 編譯成 bytecode
18│ (Compiler) │
19└─────────────┘
20 │
21 ▼
22Bytecode (.pyc)
23 │
24 ▼
25┌─────────────┐
26│ 虛擬機 │ ← 執行 bytecode
27│ (VM) │
28└─────────────┘
29 │
30 ▼
31 執行結果.pyc 檔案與 pycache
1# 當你 import 一個模組時,Python 會:
2# 1. 檢查 __pycache__ 中是否有對應的 .pyc 檔案
3# 2. 如果沒有或過期,重新編譯
4# 3. 將編譯結果存入 __pycache__
5
6# 檔案命名格式:
7# module.cpython-312.pyc
8# module.cpython-313.pyc
9
10# 手動編譯
11import py_compile
12py_compile.compile('script.py')
13
14# 編譯整個目錄
15import compileall
16compileall.compile_dir('my_package/')程式碼物件(Code Object)
編譯後的 bytecode 儲存在程式碼物件中:
1def example(x, y):
2 z = x + y
3 return z * 2
4
5code = example.__code__
6
7print(code.co_name) # example
8print(code.co_varnames) # ('x', 'y', 'z')
9print(code.co_consts) # (None, 2)
10print(code.co_code) # b'|\x00|\x01\x17\x00}\x02|\x02d\x01\x14\x00S\x00'【設計層】Stack-based 虛擬機
工作原理
Python 虛擬機是 stack-based(堆疊式):
1執行 x + y:
2
31. LOAD_FAST 0 (x) Stack: [x]
42. LOAD_FAST 1 (y) Stack: [x, y]
53. BINARY_ADD Stack: [x+y] 1import dis
2
3def add(x, y):
4 return x + y
5
6dis.dis(add)
7# 輸出:
8# 2 0 RESUME 0
9#
10# 3 2 LOAD_FAST 0 (x)
11# 4 LOAD_FAST 1 (y)
12# 6 BINARY_OP 0 (+)
13# 10 RETURN_VALUE常見 Bytecode 指令
| 指令 | 說明 |
|---|---|
| LOAD_FAST | 載入區域變數 |
| LOAD_GLOBAL | 載入全域變數 |
| LOAD_CONST | 載入常數 |
| STORE_FAST | 儲存區域變數 |
| BINARY_OP | 二元運算 |
| CALL | 呼叫函式 |
| RETURN_VALUE | 返回值 |
| JUMP_FORWARD | 向前跳躍 |
| POP_JUMP_IF_FALSE | 條件跳躍 |
【實作層】使用 dis 模組
基本用法
1import dis
2
3# 反組譯函式
4def factorial(n):
5 if n <= 1:
6 return 1
7 return n * factorial(n - 1)
8
9dis.dis(factorial)分析控制流程
1import dis
2
3def loop_example():
4 total = 0
5 for i in range(10):
6 total += i
7 return total
8
9dis.dis(loop_example)
10# 可以看到 FOR_ITER、JUMP_BACKWARD 等指令比較不同實現
1import dis
2
3# 版本 1:使用迴圈
4def sum_loop(n):
5 total = 0
6 for i in range(n):
7 total += i
8 return total
9
10# 版本 2:使用內建函式
11def sum_builtin(n):
12 return sum(range(n))
13
14print("=== 迴圈版本 ===")
15dis.dis(sum_loop)
16
17print("\n=== 內建函式版本 ===")
18dis.dis(sum_builtin)
19
20# 內建函式版本的 bytecode 更少,而且 sum() 是 C 實現【效能】從 Bytecode 理解效能
為什麼區域變數比全域變數快?
1import dis
2
3global_var = 10
4
5def use_global():
6 return global_var + 1
7
8def use_local():
9 local_var = 10
10 return local_var + 1
11
12dis.dis(use_global)
13# LOAD_GLOBAL 0 (global_var) ← 需要查找全域命名空間
14
15dis.dis(use_local)
16# LOAD_FAST 0 (local_var) ← 直接用索引存取1LOAD_GLOBAL: 需要在 globals() dict 中查找
2LOAD_FAST: 直接用索引存取陣列(O(1))為什麼 list comprehension 比 for 迴圈快?
1import dis
2
3def for_loop():
4 result = []
5 for i in range(100):
6 result.append(i * 2)
7 return result
8
9def list_comp():
10 return [i * 2 for i in range(100)]
11
12dis.dis(for_loop)
13# 更多指令,包括 LOAD_METHOD (append)、CALL
14
15dis.dis(list_comp)
16# 使用特殊的 LIST_APPEND 指令,更高效字串連接的效能
1import dis
2
3def concat_plus():
4 s = ""
5 for i in range(10):
6 s = s + str(i)
7 return s
8
9def concat_join():
10 return "".join(str(i) for i in range(10))
11
12# plus 版本每次都建立新字串
13# join 版本一次性建立【深入】Python 3.11+ 的優化
Specializing Adaptive Interpreter
Python 3.11 引入了自適應特化直譯器:
1# 針對常見模式進行優化
2# 例如:如果一個函式總是接收 int 參數
3
4def add(a, b):
5 return a + b
6
7# 前幾次呼叫:使用通用的 BINARY_OP
8# 多次呼叫後:特化為 BINARY_OP_ADD_INT查看特化指令
1import dis
2
3def example():
4 x = 1
5 y = 2
6 return x + y
7
8# 使用 adaptive=True 查看特化指令
9dis.dis(example, adaptive=True)內聯快取(Inline Caching)
1# Python 3.11+ 在 bytecode 中包含快取空間
2# 用於儲存運行時資訊
3
4def get_attr(obj):
5 return obj.value
6
7# 第一次呼叫:查找 'value' 屬性
8# 之後:使用快取的位置資訊【實戰】效能調校
使用 bytecode 分析熱點
1import dis
2import timeit
3
4def version_a(data):
5 total = 0
6 for item in data:
7 total = total + item
8 return total
9
10def version_b(data):
11 total = 0
12 for item in data:
13 total += item
14 return total
15
16# 比較 bytecode
17print("=== version_a ===")
18dis.dis(version_a)
19print("\n=== version_b ===")
20dis.dis(version_b)
21
22# 實際測量
23data = list(range(1000))
24print(timeit.timeit(lambda: version_a(data), number=10000))
25print(timeit.timeit(lambda: version_b(data), number=10000))
26# 結果相近,因為 total = total + item 和 total += item
27# 在 Python 中編譯成相同的 bytecode避免不必要的屬性查找
1import dis
2
3class MyClass:
4 def __init__(self):
5 self.value = 0
6
7 def slow_method(self):
8 for i in range(100):
9 self.value += i # 每次都要查找 self.value
10
11 def fast_method(self):
12 value = self.value # 快取到區域變數
13 for i in range(100):
14 value += i
15 self.value = value
16
17dis.dis(MyClass.slow_method)
18# 迴圈內有 LOAD_FAST (self) + LOAD_ATTR (value)
19
20dis.dis(MyClass.fast_method)
21# 迴圈內只有 LOAD_FAST (value)使用 slots 加速屬性存取
1import dis
2
3class WithoutSlots:
4 def __init__(self, x):
5 self.x = x
6
7class WithSlots:
8 __slots__ = ['x']
9 def __init__(self, x):
10 self.x = x
11
12def access_without_slots(obj):
13 return obj.x
14
15def access_with_slots(obj):
16 return obj.x
17
18# bytecode 相同,但運行時 __slots__ 更快
19# 因為不需要查找 __dict__【參考】完整 Bytecode 列表
Python 3.12 的主要指令類別:
1載入指令:
2 LOAD_CONST, LOAD_FAST, LOAD_GLOBAL, LOAD_NAME, LOAD_ATTR
3
4儲存指令:
5 STORE_FAST, STORE_GLOBAL, STORE_NAME, STORE_ATTR
6
7運算指令:
8 BINARY_OP, UNARY_NEGATIVE, UNARY_NOT
9
10跳躍指令:
11 JUMP_FORWARD, JUMP_BACKWARD, POP_JUMP_IF_TRUE, POP_JUMP_IF_FALSE
12
13函式相關:
14 CALL, RETURN_VALUE, PUSH_NULL
15
16迭代相關:
17 GET_ITER, FOR_ITER
18
19容器相關:
20 BUILD_LIST, BUILD_TUPLE, BUILD_MAP, LIST_APPEND思考題
- 為什麼 Python 選擇 stack-based VM 而不是 register-based VM?
.pyc檔案可以跨平台使用嗎?為什麼?- JIT 編譯器(如 PyPy)與 CPython 的直譯器有什麼根本差異?
實作練習
- 使用
dis比較map()和 list comprehension 的 bytecode - 寫一個簡單的 bytecode 分析工具,計算指令數量
- 研究 Python 3.11 和 3.12 的 bytecode 變化
延伸閱讀
上一章:記憶體管理與垃圾回收 下一章:GIL 與執行緒模型