Python 3.15 的 Windows x86-64 解释器有望提升 15% 运行速度

内联优化本是最有效的优化手段之一。但我们发现,编译器有时会直接拒绝将这个12k行评估循环中的最简单函数进行内联

此前我曾就 Python 尾调用结果发布过一篇致歉声明,为在未察觉编译器错误的情况下发布性能测试结果致歉。

今日我自豪地宣布部分撤回该道歉——但仅限于两个平台:macOS AArch64(XCode Clang)和Windows x86-64(MSVC)。

在我们自己的实验中,CPython的尾调用解释器在AArch64 macOS上使用XCode Clang时,在pyperformance测试中比计算型goto解释器快5%;而在Windows上使用实验性内部版本MSVC时,pyperformance测试中快约15%。Windows版本针对的是switch-case解释器,但理论上这不应造成太大影响,下节将详细说明。

元素周期表

当然,这是个希望准确的结果。我已尽力做到严谨,但人非圣贤。不过我发现及早分享并甘愿出丑往往效果不错——这常能促使他人发现我代码中的漏洞,因此我将继续坚持此做法 :)。

此外,此结论假设该变更不会在Python 3.15开发周期后期被撤销。

解释器的简要背景§

简单回顾一下。当前基于C语言编写解释器的流行方式主要有两种。

开关语句法:

switch (opcode) {
    case INST_1: ...
    case INST_2: ...
}

通过开关语句跳转到正确的指令处理程序。

另一种流行方式是GCC/Clang扩展功能——标签作为值/计算跳转。

goto *dispatch_table[opcode];
INST_1: ...
INST_2: ...

本质原理相同,但跳转目标是下一个标签的地址。传统上,其关键优化在于仅需一次跳转即可执行下一条指令,而采用switch-case的解释器中,简单编译器需要两次跳转。

然而现代编译器中,计算跳转的优势已大幅减弱,主要因为现代编译器性能提升,硬件也日益强大。在Nelson Elhage对下一类解释器的精彩研究中,现代Clang编译器上计算跳转相较switch-case的加速效果在pyperformance测试中仅达到个位数百分比。

数十年前曾提出第三种方案——基于尾调用的线程化解释器,但始终未能完全实现。该方案中每个字节码处理器独立为函数,指令流通过尾调用实现处理器间的递进:

return dispatch_table[opcode];

PyObject *INST_1(...) {

}

PyObject *INST_2(...) {
}

该方案在C语言中难以实现的核心原因在于:尾调用优化仅是可选优化。C编译器可能执行也可能不执行该优化。这意味着若不幸遇到编译器拒绝执行尾调用,解释器可能引发栈溢出!

此前Clang引入了__attribute__((musttail))属性,强制要求调用必须采用尾调用形式,否则编译将失败。据我所知,该特性首次在主流解释器中推广应用,源自Josh Haberman的Protobuf博客文章

后来,徐浩然发现GHC调用约定与尾调用结合能生成高效代码。他们在论文中将其用于基准JIT实现,并将该技术命名为复制与修补

当前进展§

在使用固定版本的XCode Clang后,我们在CPython 3.14/3.15上的性能测试表明:尾调用解释器相较于计算式跳转确实能带来适度提速。根据pyperformance平台数据,其几何平均提升幅度约为5%。

据我所知,uv已在macOS平台的Python 3.14中启用尾调用机制,这可能是该平台部分性能提升的来源。我们计划在python.org发布的官方macOS 3.15二进制文件中同样启用尾调用。

不过您关注的显然不是这个。本文标题明确指向MSVC Windows x86-64平台。那么该平台的情况如何?

Windows平台的尾调用§

[!CAUTION] 据我所知,下文讨论的MSVC特性仍处于实验阶段。除非MSVC团队决定保留,否则无法保证这些特性长期存在。使用风险自负!

以下是CPython在MSVC环境下采用尾调用与switch-case结构的初步pyperformance测试结果。任何大于1.00x的数值代表加速(例如1.01x == 1%加速),小于1.00x则表示减速。加速率几何平均值约为15-16%,范围从~60%降速(一两个异常值)到78%加速。但关键在于绝大多数基准测试都实现了提速!

图表来源:Michael Droettboom

[!WARNING] 这些结果基于实验性内部MSVC编译器,公开结果如下。

为验证结果并确保自己没有再次出错,我使用Visual Studio 2026在本地机器上复现了测试。以下数据来自此问题

Mean +- std dev: [spectralnorm_tc_no] 146 ms +- 1 ms -> [spectralnorm_tc] 98.3 ms +- 1.1 ms: 1.48x faster
Mean +- std dev: [nbody_tc_no] 145 ms +- 2 ms -> [nbody_tc] 107 ms +- 2 ms: 1.35x faster
Mean +- std dev: [bm_django_template_tc_no] 26.9 ms +- 0.5 ms -> [bm_django_template_tc] 22.8 ms +- 0.4 ms: 1.18x faster
Mean +- std dev: [xdsl_tc_no] 64.2 ms +- 1.6 ms -> [xdsl_tc] 56.1 ms +- 1.5 ms: 1.14x faster

是的,性能提升确凿无疑!对于xDSL这类中等规模库,我们观察到14%的加速效果;而在nbody和spectralnorm等小型微基准测试中,加速幅度更为显著。

感谢Chris Eibl和Brandt Bucher的协助,我们成功将此功能的PR在MSVC上推进至最终阶段。我还要衷心感谢MSVC团队。无论怎么强调都不为过:与他们合作令人愉悦,他们的工作成果令我印象深刻,在此祝贺Visual Studio 2026正式发布。

此功能现已列入3.15版本更新说明:

使用Visual Studio 2026(MSVC 18)构建时,现可启用新型尾调用解释器。早期实验性MSVC编译器的测试数据显示,相较于switch-case解释器,该功能在Windows x86-64平台上运行pyperformance几何平均值时可提升约15%的运行速度。我们在Windows平台观察到:大型纯Python库加速15%,长运行小型纯Python脚本加速达40%。(由Chris Eibl、Ken Jin和Brandt Bucher在gh-143068贡献。特别感谢MSVC团队成员Hulon Jenkins等。)

此为[[msvc::musttail]]的文档

性能提升究竟源自何处?§

我曾认为尾调用解释器的速度提升源于更优的寄存器使用。虽然我至今仍认同此观点,但怀疑这并非CPython加速的主要原因。

我目前的推测是:尾调用将编译器启发式策略重置为合理水平,从而使编译器能正常发挥作用

举个例子:截至本文撰写时,CPython 3.15的解释器循环约有12k行C代码。这意味着整个开关语句和计算跳转的解释器逻辑被塞进了一个单一函数里。

这在过去给编译器带来了诸多问题,多到无法一一列举。我将在2025年欧洲Python大会上就此发表演讲。简而言之,这个过大的函数破坏了许多编译器启发式规则。

内联优化本是最有效的优化手段之一。但我们发现,编译器有时会直接拒绝将这个12k行评估循环中的最简单函数进行内联。需要强调的是,这并非编译器的过错。它实际上做出了正确选择——通常我们不希望让本就庞大的代码进一步膨胀。但这对我们的解释器而言并非吉兆。

或许你会说:干脆用汇编写解释器不就得了!但本练习的核心意义恰恰在于避免这种做法。

闲话少叙,现在让我们审视代码。以实际示例BINARY_OP_ADD_INT(用于加法运算两个Python整数)为例。整理代码提升可读性后,其结构如下:

TARGET(BINARY_OP_ADD_INT) {
    // Increment the instruction pointer.
    _Py_CODEUNIT* const this_instr = next_instr;
    frame->instr_ptr = next_instr;
    next_instr += 6;
    _PyStackRef right = stack_pointer[-1];

    // Check that LHS is an int.
    PyObject *value_o = PyStackRef_AsPyObjectBorrow(left);
    if (!_PyLong_CheckExactAndCompact(value_o)) {
        JUMP_TO_PREDICTED(BINARY_OP);
    }

    // Check that RHS is an int.
    // ... (same code as above for LHS)

    // Add them together.
    PyObject *left_o = PyStackRef_AsPyObjectBorrow(left);
    PyObject *right_o = PyStackRef_AsPyObjectBorrow(right);
    res = _PyCompactLong_Add((PyLongObject *)left_o, (PyLongObject *)right_o);

    // If the addition fails, fall back to the generic instruction.
    if (PyStackRef_IsNull(res)) {
        JUMP_TO_PREDICTED(BINARY_OP);
    }

    // Close the references.
    PyStackRef_CLOSE_SPECIALIZED(left, _PyLong_ExactDealloc);
    PyStackRef_CLOSE_SPECIALIZED(right, _PyLong_ExactDealloc);

    // Write to the stack, and dispatch.
    stack_pointer[-2] = res;
    stack_pointer += -1;
    DISPATCH();
}

看似简单,现在观察VS 2026中switch-case的汇编实现。再次说明:此为便于调试的非PGO构建版本,PGO通常能解决部分问题但非全部:

                if (!_PyLong_CheckExactAndCompact(value_o)) {
00007FFC4DE24DCE  mov         rcx,rbx  
00007FFC4DE24DD1  mov         qword ptr [rsp+58h],rax  
00007FFC4DE24DD6  call        _PyLong_CheckExactAndCompact (07FFC4DE227F0h)  
00007FFC4DE24DDB  test        eax,eax  
00007FFC4DE24DDD  je          _PyEval_EvalFrameDefault+10EFh (07FFC4DE258FFh)
...
                res = _PyCompactLong_Add((PyLongObject *)left_o, (PyLongObject *)right_o);
00007FFC4DE24DFF  mov         rdx,rbx  
00007FFC4DE24E02  mov         rcx,r15  
00007FFC4DE24E05  call        _PyCompactLong_Add (07FFC4DD34150h)  
00007FFC4DE24E0A  mov         rbx,rax  
...
                PyStackRef_CLOSE_SPECIALIZED(value, _PyLong_ExactDealloc);
00007FFC4DE24E17  lea         rdx,[_PyLong_ExactDealloc (07FFC4DD33BD0h)]  
00007FFC4DE24E1E  mov         rcx,rsi  
00007FFC4DE24E21  call        PyStackRef_CLOSE_SPECIALIZED (07FFC4DE222A0h) 

咦……所有函数都没被内联。这肯定是因为它们体积过大之类的吧?来看看PyStackReF_CLOSE_SPECIALIZED

static inline void
PyStackRef_CLOSE_SPECIALIZED(_PyStackRef ref, destructor destruct)
{
    assert(!PyStackRef_IsNull(ref));
    if (PyStackRef_RefcountOnObject(ref)) {
        Py_DECREF_MORTAL_SPECIALIZED(BITS_TO_PTR(ref), destruct);
    }
}

这看起来……完全可以内联啊?

这是在VS 2026上启用尾调用时的BINARY_OP_ADD_INT(同样未启用PGO):

                if (!_PyLong_CheckExactAndCompact(left_o)) {
00007FFC67164785  cmp         qword ptr [rax+8],rdx  
00007FFC67164789  jne         _TAIL_CALL_BINARY_OP_ADD_INT@@_A+149h (07FFC67164879h)  
00007FFC6716478F  mov         r9,qword ptr [rax+10h]  
00007FFC67164793  cmp         r9,10h  
00007FFC67164797  jae         _TAIL_CALL_BINARY_OP_ADD_INT@@_A+149h (07FFC67164879h) 
...
                res = _PyCompactLong_Add((PyLongObject *)left_o, (PyLongObject *)right_o);
00007FFC6716479D  mov         eax,dword ptr [rax+18h]  
00007FFC671647A0  and         r9d,3  
00007FFC671647A4  and         r8d,3  
00007FFC671647A8  mov         edx,1  
00007FFC671647AD  sub         rdx,r9  
00007FFC671647B0  mov         ecx,1  
00007FFC671647B5  imul        rdx,rax  
00007FFC671647B9  mov         eax,dword ptr [rbx+18h]  
00007FFC671647BC  sub         rcx,r8  
00007FFC671647BF  imul        rcx,rax  
00007FFC671647C3  add         rcx,rdx  
00007FFC671647C6  call        medium_from_stwodigits (07FFC6706E9E0h)  
00007FFC671647CB  mov         rbx,rax  
...
                PyStackRef_CLOSE_SPECIALIZED(value, _PyLong_ExactDealloc);
00007FFC671647EB  test        bpl,1  
00007FFC671647EF  jne         _TAIL_CALL_BINARY_OP_ADD_INT@@_A+0ECh (07FFC6716481Ch)  
00007FFC671647F1  add         dword ptr [rbp],0FFFFFFFFh  
00007FFC671647F5  jne         _TAIL_CALL_BINARY_OP_ADD_INT@@_A+0ECh (07FFC6716481Ch)  
00007FFC671647F7  mov         rax,qword ptr [_PyRuntime+25F8h (07FFC675C45F8h)]  
00007FFC671647FE  test        rax,rax  
00007FFC67164801  je          _TAIL_CALL_BINARY_OP_ADD_INT@@_A+0E4h (07FFC67164814h)  
00007FFC67164803  mov         r8,qword ptr [_PyRuntime+2600h (07FFC675C4600h)]  
00007FFC6716480A  mov         edx,1  
00007FFC6716480F  mov         rcx,rbp  
00007FFC67164812  call        rax  
00007FFC67164814  mov         rcx,rbp  
00007FFC67164817  call        _PyLong_ExactDealloc (07FFC67073DA0h) 

瞧瞧这效果,简单的函数突然就能内联了 :)。

你可能会说:PGO构建肯定不会这样吧?但上面链接的问题报告明确指出确实会!所以呢,真是好日子啊。

再次强调:这绝非编译器的问题!只是CPython解释器循环本身就不太适合优化。

如何尝试?§

遗憾的是,目前仍需从源代码编译。

使用VS 2026时,克隆CPython后进行PGO优化发布构建:

$env:PlatformToolset = "v145"
./PCbuild/build.bat --tail-call-interp -c Release -p x64 --pgo

希望未来Python 3.15开发成熟后,我们能以更便捷的二进制形式分发此版本!

附录与修订§

有人要求进行交叉编译器测试。因此这里提供一个快速简易的pystones玩具基准测试。末行是启用尾调用的构建结果。所有配置均启用PGO优化。在此测试中,性能提升约30%。需注意此测试缺乏科学性——样本量仅为1,且因故无法在Windows笔记本上禁用Turbo Boost功能。

Compiler PlatformToolSet Pystones/second (higher is better)
VS2019 142 677544
VS2022 143 710773
VS2026 145 682089
VS2026+TC 145 970306
元素周期表抱枕

本文由 TecHug 分享,英文原文及文中图片来自 Python 3.15’s interpreter for Windows x86-64 should hopefully be 15% faster

共有{113}精彩评论

  1. 关键代码片段(希望博客能包含这部分):

      #   if defined(_MSC_VER) && !defined(__clang__)
      #      define Py_MUSTTAIL [[msvc::musttail]]
      #      define Py_PRESERVE_NONE_CC __preserve_none
      #   else
      #       define Py_MUSTTAIL __attribute__((musttail))
      #       define Py_PRESERVE_NONE_CC __attribute__((preserve_none))
      #   endif
    

    https://github.com/python/cpython/pull/143068/files#diff-45b

    显然(?)这还需附加在函数声明符上,且不能作为函数修饰符使用:static void *__preserve_none slowpath(); 而不是 __preserve_none static void *slowpath();(与GCC属性语法不同,后者对此类情况往往相当激进,有时会导致混淆的结果)。

    若微软认为你足够重要,就能揭露未文档化的MSVC特性,真棒啊 :/

    1. 足够重要,还是直接受益于此?我实在猜不透提升Python性能能给他们带来什么好处,但这恐怕才是真正原因。

      1. 正是微软从退休状态中招募了Guido,并携手Facebook最终启动了CPython即时编译(JIT)计划。

        Python是微软开发博客中重点推荐的语言之一。

        1. 该项目最初由Mark Shannon提出,Van Rossum主动介入。去年微软解雇了负责加速CPython的团队成员。

          五年间进展有限,偶有10-15%的性能提升,但随后又被代码膨胀抵消。

          我认为该项目始于3.10版本,因此3.9是可比的最后版本。这些改进并不显著,其他语言若取得如此微小的成果却获得如此多赞誉,恐怕难以想象。

          1. > 五年来进展有限,偶有10-15%的性能提升,但随后又被代码膨胀所抵消。

            抱歉,除非你的工作负载是基于C API的numpy数值计算器,仅在CPU上执行矩阵乘法,否则这种说法可能有误。

            仅在3.11版本中,CPython在x86-64 Ubuntu平台的pyperformance测试中较3.10提升了约25%。https://docs.python.org/3/whatsnew/3.11.html#whatsnew311-fas

            在pyperformance测试中,3.14相较于CPython 3.10在x86-64 Ubuntu平台上提升了35-45%的性能https://github.com/faster-cpython/benchmarking-public

            这些性能提升已通过外部项目验证。例如我关注的Python MLIR编译器发现,从CPython 3.10升级至3.11后几何平均速度提升达36%(详见https://github.com/EdmundGoodman/masters-project-report第49页)

            另一项学术基准测试显示,3.13相较3.10在测试套件中提升约1.8倍速度https://youtu.be/03DswsNUBdQ?t=145

            CPython 3.11的加速幅度如此显著,以致PyPy相形之下略显迟缓。不知是否还有人记得:在CPython 3.9时代,PyPy在自身基准测试套件中曾实现超过4倍的性能提升,而如今其官网https://speed.pypy.org/显示3.11版本的提升幅度已降至2.8倍。

            没错,CPython依然偏慢,但它正在变快 :)。

            免责声明:我只是志愿者而非微软员工,无需提交性能报告。以上仅为个人主观观点。

            1. 作为参考数据点,我近期开发的Python程序(几乎完全由Python代码构成,含少量I/O操作,是为最终用低级语言实现的代码所做的原型)运行结果如下:

              (macOS Ventura, x64)

              – 系统版 Python 3.9.6:26.80秒 用户时间 0.27秒 系统时间 99% CPU 27.285 总耗时

              – MacPorts Python 3.9.25:23.83秒 用户时间 0.32秒 系统时间 98% CPU 24.396 总耗时

              – MacPorts python 3.13.11: 15.17秒 用户时间 0.28秒 系统时间 98% CPU 15.675 总耗时
              – MacPorts python 3.14.2: 15.31秒 用户时间 0.32秒 系统时间 98% CPU 15.893 总耗时

              真希望我早点想到做这个测试。(我通常不太在意Python版本升级,因为我认为最优版本就是最易安装的版本,或者更理想的是直接现成的版本。我已经习惯了当前的语言和标准库,一直以为性能限制和以往一样…!)

              1. 我有个常用的基准测试程序,是2017年Advent of Code第五天的解法,纯Python实现且I/O开销可忽略。它在PyPy上的运行速度仍比Python 3.14快8.8倍:

                    $ hyperfine “mise exec python@pypy3.11 -- python e.py” “mise exec python@3.9 -- python e.py” “mise exec python@3.11 -- python e.py” “mise exec python@3.14 -- python e.py”
                    基准测试 1:mise exec python@pypy3.11 -- python e.py
                      时间 (均值 ± 标准差):     148.1 毫秒 ±   1.8 毫秒    [用户时间: 132.3 毫秒, 系统时间: 17.5 毫秒]
                      范围(最小值…最大值):   146.7 毫秒 … 154.7 毫秒    19 次运行
                
                    基准测试 2:mise exec python@3.9 -- python e.py
                      时间(均值 ± 标准差):      1.933 s ±  0.007 s    [用户时间: 1.913 s, 系统时间: 0.023 s]
                      范围(最小值 … 最大值):    1.925 s …  1.948 s    10 次运行
                     
                    基准测试 3:mise exec python@3.11 -- python e.py
                      时间(均值 ± 标准差):      1.375 s ±  0.011 s    [用户时间: 1.356 s, 系统时间: 0.022 s]
                      范围(最小值 … 最大值):    1.366 s …  1.403 s    10 次运行
                     
                    基准测试 4:mise exec python@3.14 -- python e.py
                      时间 (均值 ± 标准差):      1.302 s ±  0.003 s    [用户时间: 1.284 s, 系统时间: 0.022 s]
                      范围(最小值…最大值):    1.298 s …  1.307 s    10次运行
                     
                    总结
                      mise exec python@pypy3.11 -- python e.py 运行速度
                        比 mise exec python@3.14 -- python e.py 快 8.79 ± 0.11 倍
                        比 mise exec python@3.11 -- python e.py 快 9.28 ± 0.13 倍
                       比 mise exec python@3.9 -- python e.py 快 13.05 ± 0.16 倍
                

                https://gist.github.com/llimllib/0eda0b96f345932dc0abc2432ab

              2. > […] 我一直以为性能限制会和以往一样…!)

                历史上CPython性能一直很差,因此只要有人认真投入,实现大幅提速是完全可能的。

          2. 我知道去年发生了什么,我的重点是导致那次努力的前期背景。

            https://thenewstack.io/guido-van-rossums-ambitious-plans-for

            认同这种观点,Python是唯一看似被各种努力埋葬的动态语言。

            但问题并非出在动态特性本身——Smalltalk、Self、Common Lisp同样具备动态特性,它们同样存在重启世界、破坏即时编译努力的风险,因为任何变更都会影响整个映像。

            当然,这些语言的内部机制并未像Python那样向C语言开放,任其随意操作,其文化中C库被视为语言库本身。

            1. 呃,PHP完全符合这个描述,且显然可优化。各种方案都曾为PHP带来良好效果,包括最初的HipHop、HHVM、我自己的工作成果,以及主线PHP运行时。

              Python某些语义和行为对优化尤其不利,但正如Faster Python等项目所示,核心挑战在于兼容性——包括扩展模块,以及CPython内部对简洁实现的历史追求。

              为这些语言追溯性地打造真正高性能存在局限。你需要足够的静态、可选或渐进式类型系统,才能在常见场景下实现足够速度。这正是V8团队放弃优化转而创建Dart、Facebook团队开发Hack等项目的缘由。但值得注意的是,这些尝试均未获得真正广泛的采用。性能并非唯一考量,尤其当你已拥有成熟的代码库和生态系统时。

              1. > 你也看到V8团队放弃后开发了Dart

                即便核心团队转投Dart项目,V8仍实现了显著提速。这得益于大量运行时优化(如对象模型优化)、数个新编译器以及垃圾回收机制的改进。

                如今要让动态语言达到JS的运行速度,需要投入巨大成本。

                1. > 如今要让动态语言达到JS的运行速度需要巨大投入。

                  确实如此。但另一方面,其他语言实现(如CPython)可以借鉴JS领域积累的所有经验。

              2. > 性能并非唯一考量,尤其当你拥有成熟的代码库和生态系统时。

                这也正是Java和JS持续推动虚拟机性能进步的重要原因——存在大量用户高度关注的代码库,促使开发者持续优化性能。(尽管两者的关注点截然不同:Java更注重长期性能,JS则更侧重短期性能。)

                相较于Python等语言,它们都属于相对静态的特性也无伤大雅。

        2. + 它还获得了PowerBI的青睐,近期Excel也开始采用。

        3. 终身就业秘诀:创造一种相当优秀但存在致命缺陷(全局解释器锁、类型系统、运行缓慢)的编程语言,从此衣食无忧。

          1. 虽不敢称其为致命缺陷,但关键在于营造一种文化氛围——让人们相信这些缺陷是无法逃避的现实法则。

      2. 我猜Azure上运行着不少Python工作负载,微软提供了大量数据分析和大型语言模型工具作为服务(不按CPU分钟计费)。节省CPU周期直接转化为成本节约。

      3. 想想他们为Pylance和VAC中的通用Python支持投入了多少精力。显然他们认为用户基数足够庞大,值得打造一流的使用体验。

      4. 这或许与Excel中的Python功能有关。大量用户会在微软服务器上运行用Python编写的数值计算程序。

      5. 许多商业工程与科学软件都运行在Windows平台上。

      1. 这就是他们想用AI取代我们的原因。

      2. 我不明白你为什么立刻联想到色情内容。说实话有点毛骨悚然。

        1. 你肯定没做过公关传播工作,这种话题绝对禁忌是有道理的。

        2. 唉。最近总看到年轻人把这种过度敏感、别有用心、虚伪做作的装腔作势当成怪异的叛逆形式。这条回复就带有所有典型特征。请别把这种东西带到HN。荒谬又令人疲惫。根本没人搞怪。别再火上浇油了。

  2. 我对这事不太了解,但希望别重演Python 3.14那次闹剧——当时宣称用Clang 19编译能比标准解释器提升9-15%的几何平均速度,结果发现LLVM 19的漏洞导致基准解释器的调度循环无法正确执行“尾部复制”优化,才造成数据虚高。实际性能提升约为4%。

    编辑:仔细阅读后认为该帖完全合理且表述准确:作者明确表示其方法是“尽早分享并甘愿出丑”,优先考虑透明度和快速迭代,而非事先进行严密验证。

    固然有人会质疑他是否该进行交叉编译器检测、独立审计,或在跨平台结果万无一失前延迟发布。但鉴于他百分百公开了自己的思考过程和工作方式,这完全无可厚非。

    1. 谢谢 :),这确实是我的本意。回过头看,之前3.14版本的错误其实是件好事——若非我提前公开工作进展,就不会引起Nelson的关注。他可能也不会花整整一个月深挖Clang 19漏洞。这意味着该漏洞本不会在测试版被发现,甚至可能随正式版发布,后果将严重得多。因此事后看来,这完全是个令人欣慰的意外,我心怀感激——毕竟最终CPython整体仍从中获益!

      这次我更有把握,因为涉及两项性能优化:调度逻辑和内联处理。MSVC在满足特定条件时,可自动将switch-case解释器转换为线程化代码[1]。但它似乎并未对当前CPython解释器进行此类转换。我推测CPython解释器循环过于复杂,难以满足这些条件。关键在于我们仍需依赖MSVC的魔法处理,而尾调用方案能让C代码编写者掌握更多控制权。至于内联优化,除非使用__forceinline或改用宏实现[2],否则几乎不可能说服MSVC执行。不过我们不会在CPython中将所有函数都标记为forceinline,因为这可能对其他编译器产生负面影响。

      [1]: https://github.com/faster-cpython/ideas/issues/183 [2]: https://github.com/python/cpython/issues/121263

      1. 但愿所有自我标榜的科学家和炒作新闻的记者,能有你百分之一的诚实品格,以及对真实科学与科学传播的执着追求。你似乎认为,在这些技术细节上保持透明度,比临床医学研究者对其主张的透明度更为重要。衷心感谢你所做的一切,以及你传递知识的方式。

        另外,虽然我对整个流程不太熟悉,但想说你上次表演剧场时对自己太苛刻了。所以再次感谢你,记得别用别人都不可能达到的标准来要求自己。

        1. +1,读完帖子看到PR更新文档…感谢你的透明度,但也不要对自己这么严苛!

          那是个非常冷门的错误,你又迅速修正了,没必要为此过度道歉!感谢你为提升Python速度付出的所有努力!

        2. 非常感谢你的暖心话语,这对我意义重大!

    2. 重申我当时的观点:新设计的一大优势在于减少了优化器反复无常的影响:https://news.ycombinator.com/item?id=43322451

      若生成最优代码依赖于堆砌大量对你有利的启发式规则,你就更容易受到这些规则未来可能反转的影响。尾部复制正是我们期望的效果,但未来编译器版本可能因代码体积增大而判定其不可取。

      新设计使Python解释器能更直接地表达目标机器码形态,从而减少受优化器随意调整的影响。

      1. 是的,我认同这个观点,它似乎也适用于MSVC。顺便感谢你的工作启发了这一切!

  3. > 这在过去给编译器带来了诸多问题,多到无法一一列举。我将在EuroPython 2025大会就此发表演讲。

    看来是指这个:

    https://youtu.be/pUj32SF94Zw

    (希望文章里能直接链接)

  4. 这简直是唾手可得的优化点。核心循环怎么还没被深度优化?

    我本以为它会为主流指令集采用手工编写的汇编代码,并为非主流指令集提供C语言备份方案。

    全球因相对未优化的解释器浪费了多少能源?

    1. 恰恰相反,我认为这次更新恰恰证明了内部循环已被深度优化!

      MSVC对musttail的支持是最新推出的功能:

      > MSVC构建工具14.50版引入的[[msvc::musttail]]属性,是微软专属的实验性x64特性,用于强制尾调用优化。[1]

      MSVC构建工具14.50版上月刚发布,CPython团队仅用数周就将其转化为性能提升。

      [1] https://learn.microsoft.com/en-us/cpp/cpp/attributes?view=ms

    2. Python的初衷从来不是追求速度。若以速度为目标,它早该采用即时编译(JIT)技术,而非纠结于优化解释器。Guido始终将代码简洁性置于速度之上。包括JIT在内的诸多性能改进(PEP 744——即时编译),都是在他卸任后才实现的。

      1. 我怀疑它早该有JIT了。问题在于,虽然人们为Python开发JIT编译器已有多年,但语言本身的语义特性使得难以从中获益——因为大部分耗时并非来自字节码解释器本身,而是调度过程。人们常将Python与JavaScript相提并论,但Python的灵活性远超后者——例如所有“基本”类型都是可继承的对象,就连属性查找这类基础机制也提供了大量定制钩子。

        因此核心问题在于:对Python而言,简单的JIT并无实际效益。要实现典型工作负载下几百分比的性能提升,必须投入大量时间精力。或者必须收紧语言规范和/或破坏C语言ABI,但这将导致众多流行库失效。

      2. 值得一提的是,Guido最终加入了负责开发相当可靠JIT引擎的团队。不过微软后来因裁员打乱了计划,目前进展尚不明确。

      3. Python充斥着这类抉择——或者说充斥着“只要多花点功夫就能提升十倍性能”的遗憾

      4. 若性能曾是目标…甚至曾被纳入考量,这门语言的形态将截然不同。

    3. 这(a)远超开源项目的预期(b)维护起来极其痛苦(c)甚至不是Python最大的时间杀手——真正的罪魁是打包“系统”。

      1. 甚至不是Python最大的时间杀手——包装“系统”才是。

        对于频繁运行的短脚本:启动时间才是关键!每次导入都必须扫描上亿个目录来定位模块位置,即便是解释器自带的标准模块也不例外。

          1. 这功能来得越快越好。Python在命令行界面表现出色,直到你构建复杂系统时才发现,一个简单的–help命令竟要耗费数秒。若不让代码变得臃肿不堪,这问题很难轻松解决。

    4. 大概是因为关注性能的人压根不会在Windows上运行工作负载吧。

      1. 游戏和Proton。

        显然关注性能的人确实会用Windows。

        1. 这些游戏中没有一款(或极少数)是用Python编写的。那些需要高性能的游戏绝对没有。

          1. 游戏整体上并非用Python编写,但Python常被用作脚本语言。虽然如今它不如从前流行——主要是Lua的崛起所致——但这种情况依然存在。

          2. 确实如此,但问题是关于整体性能表现。

        2. 游戏选择Windows平台开发,是因为设备驱动程序历来都基于该系统。其他观点都无视现实。

          1. 当然,在加载Proton时请继续坚持这个信念吧。

      2. 大量数字音频工作站、图像编辑和视频剪辑都在Windows平台运行。

    5. 软件变得如此迟缓,以至于我们都忘了计算机本该有多快

    6. 想要速度就用PyPy,别管CPython了。

  5. 在多年劝阻后,我终于放弃惯用的C#/MAUI,转而用Python开发Windows GUI应用。我对Python更熟悉,而VS整套生态系统处理轻量任务实在过于臃肿。最初尝试tkinter,但发现它在字段变更等高频交互场景下极其笨拙,而学习QT的门槛又超出我的兴趣范围。(或许两者我都掌握得不够熟练?)转而采用wxglade,通过wxpython拖放式构建界面,仅需pip安装一个外部依赖,比手写xaml便捷得多,操作体验也比QT更符合Python的编程风格。很高兴看到Windows运行时获得更多支持,未来我可能会更依赖它。

    1. 我非常喜欢Python + Qt/pyside的组合。用QtCreator快速搭建粗糙的GUI后,就能用Python飞速编写应用逻辑。

      1. 若我更频繁开发GUI应用,这绝对是首选方案,毕竟它显然更稳健可靠。目前wxglade作为拖放式设计工具表现出色,其代码风格与常规Python编程方式足够接近,省去了额外的学习成本。

    2. 根据GUI对你的重要程度,对于脚本化但需求较重的场景,建议考虑LINQPad。

    3. 也看看pyfltk吧。虽然没用过Windows版本,但在GNU/Linux上表现相当出色。

    4. 等你见到Python的ImGui绑定库[1]就知道了。它采用即时模式而非Tkinter/Qt/Wx的保留模式。若需交付厚客户端给客户可能不适用,但作为内部工具简直太棒了。

          imgui.text(f“计数器 = {counter}”)
          if imgui.button(“递增计数器”):
              counter += 1
      
          _, name = imgui.input_text(“请输入姓名?”, name)
          imgui.text(f“你好 {name}!”)
      
      

      [1] https://github.com/pthom/imgui_bundle

      1. 这似乎非常适合内部用户——他们只需运行带选项的shell脚本,属于“技术水平足以严格遵循指令,但不足以熟练/可靠地使用命令行”的群体。

  6. 我有疑问——虽略离题但相关。我一直在想:既然JavaScript和Python都是动态解释型语言,为何Python解释器比V8 JavaScript解释器慢得多?

    1. 我能想到几个原因:

      JavaScript采用即时编译(JIT),而CPython没有。PyPy虽有JIT且运行更快,但可能与C扩展不兼容。

      Python的多线程模型也增加了优化复杂度,而JavaScript的单线程架构更易优化。

      此外,CPython的优化动力普遍不足。至少在WebAssembly出现前,JavaScript性能基本受限于解释器本身。而Python有更多分流途径:纯Python开发可采用PyPy,计算密集型任务可交由C扩展处理。

      我认为某些语言特性使JavaScript更易优化,但对此我并非权威发言者。

      1. > 我认为优化CPython的动力通常较弱

        尽管如此,微软耗费四年时间组建了专门的“加速CPython”团队——他们原本计划实现5倍性能提升,最终却仅达到约1.5倍。为何他们无法打造出显著更快的Python实现方案?尤其当PyPy的存在已证明这种可能性时?

        1. PyPy的C语言交互性能远逊于CPython,我认为这是权衡取舍的结果。例如数据分析管道在CPython的numpy中运行速度可能仍快于PyPy。

          我虽非专家,但理解Python的动态特性使其难以优化。例如允许一个命名空间修改另一个命名空间;我上次使用时,Stackdriver日志适配器会覆盖Python标准库的日志库。导入stackdriver后,它会将日志发送至stackdriver。

          所有包级别的名称(函数和变量)本质上都是可变的全局变量。

          我推测大幅提升Python速度的关键在于限制某些极端可变性。例如包级函数和变量应禁止直接修改,只能通过新变量进行包装。

    2. 我认为存在两个可能原因:

      首先是谷歌的人力优势。谷歌总能成功开发出高效软件。相较于其他生态系统,我使用的多数谷歌产品都运行迅捷。或许谷歌只是做得更好。

      其次是CPython的历史包袱。虽然存在完全实现API的更快Python实现(如PyPy),但庞大的C扩展生态依赖CPython绑定,使得兼容性几乎不可打破。这种历史遗留可能阻碍了诸多优化方案。反观V8引擎,只需保持代码层面的兼容性,这使其能够逐步替换内部实现,持续寻找更高效的版本。

      以上观点仅供参考,可能存在偏差。

        1. Unladen Swallow虽被大肆宣传,实则规模极小。据我所知,仅有两名实习生参与开发。

          V8引擎的优先级则高出许多——谷歌网罗全球顶尖虚拟机工程师投入研发。

    3. > 为何Python解释器远逊于V8 JavaScript解释器?两者同属动态解释型语言。

      因为JavaScript对网络的核心地位,以及V8速度对谷歌避免其他平台通过默认浏览器掌控网络的战略意义,使得谷歌在JavaScript语言本身基本静态化的时期,投入了近乎无限的资源优化V8;而Python从未获得同等投入,始终将有限资源部分用于语言本身的发展而非实现优化。

      此外,需要支持的JS历史遗留代码是JS,而CPython还存在大量与Python深度集成的外部代码生态,这些代码使用的接口限制了可施加的优化。虽然存在不支持外部生态的更快Python解释器,但由于该生态是Python价值主张的重要组成部分,这类解释器使用较少。

    4. 需注意的是,除谷歌等巨头对JS运行时解释器的资金投入外,Python作为语言本身就比JavaScript更具“动态性”。

      即便像字段访问这类“简单”操作,在Python中也可能涉及多重动态映射的方法解析。

      此外,Python的ffi绑定虽能通过C/C++/Fortran等语言库扩展功能,却限制了内部机制的自由修改空间(例如PyPy为实现逐个漏洞兼容性所做的努力,某些优化方案便受此约束)。

      1. > Python作为语言本身远比JavaScript更“动态”

        此言极是,但PyPy的存在证明这并不必然妨碍实现高效运行。我认为CPython性能欠佳的根源在于你提到的另一点:

        > Python的ffi绑定[…]限制了内部实现的自由度

      2. 真心好奇,理论上JS的代理对象和原型链机制是否也会造成类似的性能影响?

        1. 是的,就动态性而言,我看不出Python与JavaScript在根本上有何不同。Python确实支持运算符重载,但JavaScript会将其实现为常规方法。Python的init和new操作符并不比JavaScript的构造函数更复杂。Python虽支持多重继承,但方法和属性的解析机制仅使用MRO(方法解析序列),这与JavaScript的原型链并无二致。

    5. Python的动态性强得多。例如看看访问对象属性的基础操作:https://docs.python.org/3/howto/descriptor.html

      此外Python拥有事实上的稳定(或近乎稳定)C ABI扩展接口,其优势在于:1) 被主流库广泛采用;2) 使JIT编译器难以应对——原生代码对Python对象具备完全相同的表达能力,但JIT无法通过代码分析确保其不被使用。

    6. 尽管JavaScript相当动态,但Python的表现要差得多。基本上所有操作都涉及运行时查找。这简直是刻意追求极致低效时会设计的语言。

  7. 我从未见过这种基准测试图表,看起来非常酷!这是如何生成的?使用了什么工具进行测试?

    (其实我整个9-10月都在优化 Immer JS 不可变更新库,使用名为 `mitata` 的基准测试工具,所以做了大量类似工作:https://github.com/immerjs/immer/pull/1183 。很想把新工具加入我的工具库!)

    1. 您指的是小提琴图吗?https://en.wikipedia.org/wiki/Violin_plot 以及Matplotlib中的实现https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot….

      本质上是带平滑处理的分布直方图,且两侧对称镜像。

      虽然视觉效果美观,但存在合理争议:1) 平滑处理可能掩盖真实分布特征;2) 镜像处理未增加信息量却占用空间,且暗示额外空间蕴含信息;3) 垂直展示时常引发“形似女性生殖器”的惊呼。

      在Hacker News相关讨论中,用户medstrom于https://news.ycombinator.com/item?id=40766519提出半小提琴图方案,详见https://miro.medium.com/v2/1* J3Q4JKXa9WwJHtNaXRu-kQ.jpeg 展示了左侧的直方图与右侧的半小提琴图,让您得以并排观察同一数据的两种呈现方式。

      1. 直方图未必能真实反映分布特征。分箱数量或箱宽会显著影响显示细节。

  8. Python的近期发展堪称里程碑,新版本在M4 MacBook Air上的性能表现已轻松超越PyPy图表。不知是否与苹果的优化有关,但作为Linux用户我深感意外。

  9. Matt Godbolt近期指出,解释器采用尾调用机制更契合CPU内部的分支预测器,相较于单个大型switch/计算跳转更具优势。

  10. 首句有两处拼写错误。这是刻意为之,让它明显不像AI生成的内容吗?
    “apology peice”和“tail caling”

    1. 若想让文章看起来非AI生成,最简单的方法就是亲手撰写。根本不需要刻意出错。

      我相信只要足够引导,大型语言模型也能吐出技术博客——只要避免那些明显粗制滥造的特征:滥用表情符号、陈词滥调、自我吹捧、过度欢快的语气、短促“有力的”段落、缺乏深度, “这不仅是X——而是全新的Y”——但这至少有点棘手,毕竟人们常懒得费心。

      [ChatGPT,在此插入一句抱怨:人们总要把LLM硬塞进每场讨论,无论多么无关。]

  11. 简而言之:尾调用解释器比计算跳转稍快。

    > 我曾认为尾调用解释器的速度提升源于更优的寄存器使用。虽然我至今仍认同此观点,但怀疑这并非CPython加速的主要原因。

    > 我现在的核心推测是:尾调用将编译器启发式策略重置为合理水平,从而让编译器能正常发挥作用。

    > 举例说明:截至撰写本文时,CPython 3.15的解释器循环约有1.2万行C代码。这意味着开关语句和计算跳转解释器需在单个函数中处理1.2万行代码。

    > […] 简言之,如此庞大的函数破坏了大量编译器启发式规则。

    > 最有益的优化之一是内联。过去我们发现,编译器有时会直接拒绝在那个12k行代码的评估循环中内联最简单的函数。

    1. 我认为在protobuf示例中,musttail确实受益于更优的寄存器使用。所有函数调用参数完全相同,因此无需重新分配寄存器。相同六个寄存器参数在函数间循环复用。

  12. 据我理解,这种基于尾调用的解释方式对分支预测器也更友好。这或许能解释部分性能下降的原因——它们触发了导致大量分支预测失误的特殊场景。

  13. 毕竟还是Python,不过是自我超越罢了

  14. 那么…既然Python团队认为尾调用有价值,何时才能在Python中见到它们?

  15. MSVC上的测试结果相当出色。尾调用能有效重置编译器启发式策略并解除内联限制的观点颇具说服力。不过我担心的是对MSVC未文档化行为的依赖——若该特性广泛应用,CPython可能最终依赖于实际并不稳定的优化器保证。好奇你们如何考虑长期可维护性及对调试/性能分析的影响。

    1. 感谢阅读!目前CPython仍保留全部三种解释器。我们短期内不会移除其他解释器,可能永远不会。若MSVC破坏尾调用解释器,我们只需恢复构建分发switch-case解释器。Windows二进制文件速度会再次下降,但这就是现实 :(。

      此外,解释器循环的调度机制是自动生成的,可通过配置标志选择。因此几乎不存在额外维护开销。主要负担在于为实现该功能所需的MSVC特定修改(约数百行代码)。

      > 对调试/性能分析的影响

      我认为不应存在影响,至少在Windows平台上如此。不过无法完全确定。

      1. 这很合理,感谢详细说明。将开关语句解释器作为后备方案并保持调度自动生成,确实降低了长期风险。

      1. 澄清一下,这些注释是我亲自编写的。虽然使用语法LLM插件优化了措辞,但核心内容均出自本人。

  16. 说实话,比慢得要命快15%还是慢得要命

    1. 没错,但逐年提升5%到15%的速度才是实质性进步,这正是Python庞大用户群此刻所期待的——而且他们似乎正在获得这种提升!完全坦白:我并非重度Python用户,正是因为性能和构建/分发问题——从用户角度看实在遗憾(此处不讨论集中式网络部署,而是更关注去中心化分发,我认为这更“真实”且更有价值)。

  17. 现在必须查明subparsers测试为何变慢60%…(图表中0.3960处)

  18. Windows平台有Clang编译版本吗?我正逐步将Windows构建从MSVC迁移至Clang,后者仍使用微软STL实现。

    目前看来用Clang替代MSVC编译器绝对是明智之选?虽说差距不大,但终究是进步。

  19. MSVC生成的代码普遍比gcc/clang慢,这个技巧或许能缩小差距。

      1. 我十年前的经验是比GCC慢10%-15%。

  20. 若博主看到此帖:能否开通RSS订阅?

  21. 我不理解为何执着于微性能细节…毕竟这本质上是解释型方法,相对而言注定效率低下。真正的提速之道是全局JIT编译,那样就无需纠结开关循环的结构设计了。

    1. 你可能会惊讶于单纯对Python字节码进行JIT编译能带来的微小性能提升。它本身是如此高级的抽象层,真正有趣的事情大多发生在底层实现中。

  22. Python解释器的核心循环似乎是AlphaEvolve的完美应用场景。或者如果DeepMind不想为竞赛加速Python,也可以用开源替代方案OpenEvolve。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

你也许感兴趣的: