Python 3.15 超低开销性能分析模式揭秘
Python 3.15's Ultra-Low Overhead Interpreter Profiling Mode – Ken Jin's Blog
在 Python 3.15 的 JIT 开发中,我探索了一种全新的解释器性能分析模式。传统的方案要么需要维护两套解释器导致代码膨胀,要么通过布尔判断引入分支预测开销。我们最终采用了“双重分发”(Dual Dispatch)策略,通过动态切换分发表(dispatch tables)来无缝进入和退出分析模式,无需任何条件分支。实测表明,这种模式仅带来约 4.5 倍的开销,远低于 PyPy 等系统数百倍的慢速。虽然这套机制优雅高效,但我也在反思:为了这种极致的性能,我们是否引入了过高的复杂性?
我偶尔也会问自己:我们在 CPython 中构建的这个神奇系统,真的值得付出如此高的复杂性代价吗?
HN 评论区
12- NeutralForest
这里有一份由该项目参与者提供的用法说明:https://www.youtube.com/watch?v=f1x4X83CDSA,以及 3.15 测试版中配套的详细文档:https://docs.python.org/3.15/library/profiling.sampling.html
- evomassiny
能不能在每个 `INSTRUCTION_N` 之前复制一份 `RECORD_INST`,
使得:
```
INSTRUCTION_1:
// 子程序 1
ip++;
goto *dispatch_var[*ip];
INSTRUCTION_2:
// 子程序 2
ip++;
goto *dispatch_var[*ip];
```
变成:
```
RECORD_INST_1:
// 记录逻辑
INSTRUCTION_1:
// 子程序 1
ip++;
goto *dispatch_var[*ip];
RECORD_INST_2:
// 记录逻辑
INSTRUCTION_2:
// 子程序 2
ip++;
goto *dispatch_var[*ip];
```
这样你就可以直接把 `dispatch_var` 替换成一个填满了 `RECORD_INST_*` 标签的数组,从而省去运行时的一步操作。
或者,这样做是不是你为了避免增加二进制文件大小而刻意避免的?
- abbeyj
我不太明白为什么 `RECORD_INST` 处理程序里会有 `ip++`。我们在普通调度表的处理程序末尾运行 `ip++` 时,已经移动到了下一条指令。在 `RECORD_INST` 处理程序中,我们执行的是记录指令的工作,而不是执行该指令的实际工作。我们不能这么做,因为我们只有一个必须适用于所有指令的处理程序。我们是不是应该在不增加 `ip` 的情况下跳转到 `DISPATCHER_TABLE_NORMAL`?
- haeseong
即使 `bool profile` 分支预测完全准确,它依然代价高昂,因为它会膨胀每个操作码处理程序。在计算跳转(computed goto)解释器中,这些额外的代码会扰乱每个调度点建立的分支预测历史,而这正是调度速度快的原因所在。直接替换整个表可以规避这个问题。我喜欢的一点是,将所有操作码扇出到一个记录指令,然后再通过真实的表扇出,正是这一点防止了第二个表变成像早期方案那样的第二个完整解释器。