AIGC标识 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/次。这笔钱买的是:

  1. 栈回溯构造——沿调用栈逐层创建 traceback 帧链
  2. 异常表匹配——按当前指令偏移查 co_exceptiontable,逐条匹配区间
  3. 异常对象传递与匹配——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 之争就有了量化答案。

posted @ 2026-10-25 15:35  小小张说故事  阅读(10)  评论(0)    收藏  举报