Python 的 try 为什么变免费了?拆开 3.11 零成本异常:异常表、562 纳秒与字典查找的反转
Python 3.11 有个少有人讲清的变化:try 块在正常路径上不再收门票了。实测(CPython 3.13.14)带 try 的函数 147.8 ns/次,不带 try 的 151.7 ns/次——差额 -3.9 ns,落在噪声里。与此同时,真正抛出一次异常要 562 ns,是正常路径的 3.8 倍。一文拆开这套「进入免费、使用昂贵」的新机制。
一、结论先行
- 3.11 起,try 的正常路径成本 ≈ 0(实测差额在噪声范围内)
- 异常触发路径 562 ns,约为正常执行的 3.8 倍——异常是「异常情况专用通道」,不是廉价控制流
- 机制上,
SETUP_FINALLY压栈处理器被异常表(co_exceptiontable)取代,入口只在抛出异常时按表匹配 - 由此产生一个反直觉的实战结论:命中的字典查找,try/except 反而最快;未命中则最慢
二、字节码:异常表长什么样
同一个函数 f(x) { try: return x+1; except Exception: return -1 } 编译后:
L1: LOAD_CONST 1; BINARY_OP 0 (+)
L2: RETURN_VALUE
-- L3: PUSH_EXC_INFO
CHECK_EXC_MATCH
POP_JUMP_IF_FALSE (to L5)
POP_EXCEPT
L4: RETURN_CONST 2 (-1)
L5: RERAISE 0
...
ExceptionTable:
L1 to L2 -> L3 [0] ← 受保护区间 → 处理器
L3 to L4 -> L6 [1] lasti ← 处理器自身也受保护
L5 to L6 -> L6 [1] lasti
关键在 ExceptionTable:L1 到 L2 这段字节码受保护,一旦区间内任何指令抛出异常,跳到 L3 处理。正常执行时,这段表根本不参与——这就是「零成本」的字面含义。
对比 3.10 及以前:进入 try 要执行 SETUP_FINALLY,把处理器压进运行时栈,每条路径都付钱。
三、正常路径实测:一分钱不收
1M 次调用,函数体同为 i * 2:
| 写法 | ns/次 |
|---|---|
| 带 try/except | 147.8 |
| 不带 try | 151.7 |
差额 -3.9 ns——负数,即统计噪声。3.11 之前这个差额是正值(当年实测约 +20 ns 量级)。零成本异常表把「设防」的开销从每次执行挪到了每次异常。
四、异常路径:562 ns 买什么
raise ValueError 从内层函数穿透到外层 catch,实测 562 ns/次。这笔钱买的是:
- 栈回溯构造——沿调用栈逐层创建 traceback 帧链
- 异常表匹配——按当前指令偏移查
co_exceptiontable,逐条匹配区间 - 异常对象传递与匹配——
CHECK_EXC_MATCH做isinstance级联比较
细分证据:裸 raise 重抛(719 ns)比显式 raise KeyError(...) 新建异常(889 ns)便宜 22%——省下的是异常对象构造与首次回溯填充。
五、字典查找的反转:命中越快,未命中越慢
零成本异常制造了一个反直觉的场景。10 键规模字典、20 万次查找:
| 写法 | 命中 | 未命中 |
|---|---|---|
try: d[k] / except KeyError |
158 ns | 450 ns |
if k in d |
199 ns | 174 ns |
d.get(k) |
162 ns | 180 ns |
解读:
- 命中场景 try 最快——因为正常路径零成本,
d[k]一次哈希直接返回,连in的第二次哈希都省了 - 未命中场景 try 最慢——450 ns,是
.get()的 2.5 倍,异常路径的 562 ns 在这里原形毕露
所以规则不是「永远用 try」或「永远别用 try」,而是:按你的键命中率选。高命中率用 try/except,可能未命中用 .get() 或 in 预检。
六、finally 与 else:语义级保障
finally 在 return 之前执行——实测 try: return 'ret' / finally: order.append('finally'),finally 一定跑。代价也在这条路径上:return 前要先执行 finally 块的字节码,RETURN_VALUE 被改写为先跑清理。
else 子句的价值在语义:except 没抓住时代码不执行、finally 一定执行,三者构成「成功路径/失败路径/必然路径」的完整划分。想只在无异常时跑的代码,放 else 比放 try 尾部更安全(try 尾部抛的异常会被自己的 except 抓走)。
七、实战守则
- 热点路径加 try 不心疼:3.11+ 正常路径免费,过去「try 有开销所以拆掉」的说法已过时
- 但别把异常当控制流:562 ns 一次,比正常路径贵近 4 倍。EAFP 风格要按命中率选:大概率命中用 try,大概率未命中用
.get()/in - 异常类层次影响匹配成本:
except AppError(近)比except Exception(远)少走几级 MRO 比较,实测差约 4% - 重抛用裸
raise:别raise e,前者保留原 traceback 还便宜 22%
一句话总结:3.11 把 try 从「每次执行都设防」改成「只在出事时查表」——进入免费,使用 562 ns。理解了这条边界,EAFP 与 LBYP 之争就有了量化答案。

浙公网安备 33010602011771号