Python 先把原始碼編譯成 bytecode,再由虛擬機執行。理解這個過程有助於優化程式碼效能。

先備知識

本章目標

學完本章後,你將能夠:

  1. 理解 Python 的編譯流程
  2. 使用 dis 模組分析 bytecode
  3. 從 bytecode 角度理解效能差異
  4. 了解 Python 3.11+ 的效能優化

【原理層】Python 的執行流程

編譯與執行

 1原始碼 (.py)
 2 3 4┌─────────────┐
 5│   詞法分析   │ ← 將原始碼分解成 tokens
 6│   (Lexer)   │
 7└─────────────┘
 8 910┌─────────────┐
11│   語法分析   │ ← 建立抽象語法樹 (AST)
12│   (Parser)  │
13└─────────────┘
141516┌─────────────┐
17│   編譯器    │ ← 將 AST 編譯成 bytecode
18│  (Compiler) │
19└─────────────┘
202122Bytecode (.pyc)
232425┌─────────────┐
26│   虛擬機    │ ← 執行 bytecode
27│    (VM)     │
28└─────────────┘
293031  執行結果

.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

思考題

  1. 為什麼 Python 選擇 stack-based VM 而不是 register-based VM?
  2. .pyc 檔案可以跨平台使用嗎?為什麼?
  3. JIT 編譯器(如 PyPy)與 CPython 的直譯器有什麼根本差異?

實作練習

  1. 使用 dis 比較 map() 和 list comprehension 的 bytecode
  2. 寫一個簡單的 bytecode 分析工具,計算指令數量
  3. 研究 Python 3.11 和 3.12 的 bytecode 變化

延伸閱讀


上一章:記憶體管理與垃圾回收 下一章:GIL 與執行緒模型