最近刷技术资讯的话,应该没少看到“Python 3.13性能提升巨大”“速度直追C语言”这类说法。作为一个把Python当主力语言用了快十年的开发者,我对这类宣传词的第一反应是:先看源码,再看benchmark脚本,最后拿自己项目跑一遍。毕竟性能这事的体感差异太大了,同一个版本,写爬虫的和写数值计算的人能给出完全相反的评价。
这篇文章就围绕Python 3.13的性能提升来聊。我会从官方release notes里的真实数据出发,把第三代自适应解释器、copy-and-patch JIT、无GIL构建这几块核心内容拆开讲清楚,再补上一些散落在文档角落但实际有用的优化细节,最后给出我自己升级前后的实测建议。适合正在犹豫要不要从3.11/3.12迁移到3.13的人,也适合想搞明白“Python性能优化到底优化在哪”的读者。
1. 先泼盆冷水:网上说的“性能翻倍”到底靠不靠谱
1.1 官方benchmark给出的真实涨幅
先说结论:Python 3.13相对3.12的性能提升,官方pyperformance基准测试给出的数据大概是平均5%左右,部分热点场景能到10%-30%,但离“翻倍”差得很远。
要知道Python 3.11发布时官方宣称相对3.10提升了10%-60%,那是一个大版本叠加了两个大版本才换来的“明显体感”。3.12相对3.11又挤了一点牙膏,到3.13这个版本,GIL默认还在,JIT默认还是实验性开关,常规构建下的纯CPU性能进步属于“稳步推进”级别。
我见过不少测试截图,测出来3.13比3.12快了50%甚至更多。这种结果通常不是造假,而是选了非常特定的微基准,比如把同一个纯Python循环跑几十万次,或者在已经高度特化的容器操作上来回跑。这种benchmark能说明JIT和特化解释器在工作,但没法代表你手上的业务代码。
实际体感更接近这样一个规律:代码越简单、循环越密集、越“只靠Python自身运算”的项目,3.13的提升越明显;一旦项目里80%的时间消耗在numpy、pandas、数据库驱动这些C扩展库里,那3.13带来的整体收益可能连2%都不到。
1.2 为什么有人会测出“翻倍”的效果
这个问题值得拆一拆,因为它直接关系到你怎么看待性能测试。
有一种情况是拿3.13的free-threaded无GIL版本去跑多线程CPU密集任务。GIL去掉之后,4核机器上跑满4个Python线程,本来只能占到1个核的量,现在能占到多个核,测出来“2倍”“3倍”都不稀奇。但那是并行能力提升,不是单线程执行速度提升,两者必须分开评价。
另一种情况是打开了JIT之后,某些热点函数的执行路径从“解释器循环”变成了“机器码直接执行”。这种场景下测出20%-50%的提升是有可能的,尤其在函数调用密集、属性访问密集的代码里。但JIT对不热的一次性代码毫无帮助,对已经跑到C库里的代码也没有帮助。
还有一种情况纯粹是版本叠加效应。比如你拿3.10和3.13对比,3.11已经快了一大截,3.12又优化了一部分,3.13再叠加一点,总体看上去确实有接近翻倍的数据。但那是三个大版本的总和,不是3.13这一个版本的功劳。
所以在看任何“性能提升”宣传时,先问三个问题:基准是什么版本?跑的是什么类型的工作负载?用的什么构建(标准构建、JIT构建还是无GIL构建)?把这三点对齐了,才能判断一个版本对你到底有没有意义。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 第三代自适应解释器:底层字节码执行方式变了
2.1 从按字节码逐条执行到“带缓存的特化执行”
聊Python 3.13性能,绕不开PEP 659在3.11版本引入的自适应特化解释器(specializing adaptive interpreter)。这个机制到3.13已经是第三代了,也是当前默认构建下性能提升的主要来源。
理解它的最好方式,是拿一个简单的属性访问例子拆开看。你在代码里写obj.attr,编译器会生成一条LOAD_ATTR字节码。在3.11之前,解释器每次执行到这条指令,都要走一遍完整的属性查找流程:拿到对象的类型指针、在类型里查MRO、找描述符、判断是否需要调用__getattribute__……一套流程下来几十上百个C函数调用。
但如果这个obj在生产环境里几乎永远是同一个类,那这套完整流程里90%的步骤是白做的。自适应解释器做的事情就是:先按通用方式跑几遍,收集运行时类型信息,然后根据这些信息把字节码替换成更专门的版本。
以LOAD_ATTR为例,替换后可能变成LOAD_ATTR_INSTANCE_VALUE,这条特化指令直接通过内联缓存里保存的偏移量,从对象内存中取出属性值。缓存里存了对象类型指针、缓存版本号、属性在实例内存里的偏移量,几步C代码就完了,性能差距不是一个量级。
3.13的第三代自适应解释器相比3.11的初代,主要改进在两块。第一是特化覆盖的指令种类大大增加,尤其是CALL指令,覆盖了更多内建函数和方法调用的快速路径;第二是引入了第二层解释器(tier 2)和微操作(uop)机制,把字节码先翻译成更细粒度的微操作序列,再针对微操作序列做进一步优化。你可以理解为一个工厂先给零件分类,再针对每类零件设计流水线,比直接对产品做统一处理要高效得多。
2.2 3.13里新增了哪些关键特化路径
具体到3.13,有几个特化点对日常代码帮助很大,值得单独提一下。
第一个是列表的append和pop。list.append可能是Python代码里出现频率最高的方法,但方法调用的开销其实很高:要查找方法、绑定方法、检查参数、再进入C函数。3.13对list.append(元素)这类直接在循环里出现的调用做了特化。解释器发现列表对象类型固定、方法就是内建的append之后,会把整条调用路径压缩成一次快速的C函数调用,省掉中间所有查找和绑定步骤。对那种“循环里不停地往列表里塞数据”的代码,提升非常明显,我自己拿一个构造列表的测试脚本跑过,能快20%以上。
第二个是字典读写。字典的键查找在3.13里对字符串键、简单对象键都做了更激进的缓存处理,命中缓存时几乎只做一次哈希比较就得到结果。实际应用里,字典操作占大头的数据处理代码,能明显感受到变化。
第三个是内建函数调用。比如len()、isinstance()、type()这类高频函数,3.13提供了一些专门的快速调用路径。注意这些快速路径都有前提条件,比如参数类型符合预期、不需要处理关键字参数等,一旦条件不满足,解释器会自动退回到通用路径。
这里想强调一个概念:去特化(de-optimization)。特化不是永久性的,一旦运行时发现对象的类型变了、缓存的版本号失效了,解释器会丢弃这条特化指令,回到通用版本重新执行并重新收集信息。这种设计保证了特化在动态场景下不会导致错误,代价是频繁的类型变化会带来额外的反复切换开销。所以如果你的代码里同一个变量一会儿是A类对象一会儿是B类对象,那不要指望特化帮你提速,反而可能变慢。
3. copy-and-patch JIT:一次“轻量级”自带编译器的尝试
3.1 为什么CPython选了一条和PyPy完全不同的路
聊Python 3.13绕不开的另一个重头戏,就是实验性的JIT编译器。这里必须先做一个铺垫:JIT在Python生态里不是什么新鲜东西,PyPy一直靠JIT吃饭,但也一直因为兼容性和启动时间问题没法成为官方替代品。
CPython团队这次对JIT的态度非常务实:不要试图造一个能证明复杂优化的大而全的JIT,只需要在解释器成本最重的地方,用机器码替换掉微操作执行循环,能省一点是一点。
所以3.13的JIT选了个“轻量级”路线,官方术语叫做copy-and-patch JIT。这个方案不需要在运行时分析大规模数据流,也不需要维护复杂的优化流水线,它的核心思路是:提前把CPython里每个微操作(uop)对应的机器码模板生成好,等程序跑起来之后,遇到一段热点代码,就把这些模板按顺序拼接成一段可执行的机器码函数。
这段描述看起来简单,但背后有一个关键的工程决策。CPython选择了LLVM作为模板生成工具,在编译CPython本身的时候,就用LLVM把每个预定义的模板函数编译成机器码,存进一个特殊的“模板文件”里。程序运行时,JIT只需要把这个机器码模板文件加载内存,然后按需“复制”对应片段的机器码,“修补”其中的常量和跳转地址,拼成一个新函数。整个过程不需要嵌入一个传统编译器,启动时间和内存开销都要小得多。
打个比方:普通JIT像是从零开始给每个户型设计一套家具,copy-and-patch则是把定制好的标准家具模块直接搬进不同的房子,只需要微调尺寸和位置。这个思路保证了JIT不会显著拖慢Python的启动速度,也适合CPython这种“默认集成、可选开启”的定位。
3.2 copy-and-patch的底层原理:把机器码当乐高拼
我尽量用不夸张的方式描述它的工作过程,细节大致是这样的。
当tier 2解释器觉得某段代码值得编译时(阈值由内部计数器触发),JIT会拿到这段代码对应的微操作序列。每个微操作在模板文件里都有一段对应的机器码,这段机器码不是普通汇编,而是精心设计成可以被“复制后修补”的布局。JIT做的事非常机械:
- 在内存里分配一块可执行的内存区域。
- 把每个微操作对应的机器码片段依次拷贝进去。
- 修补片段与片段之间的跳转偏移量、片段内的常量池引用和栈偏移量。
- 将整段可执行代码以函数指针的方式挂到代码对象上,下次再执行这段代码,直接调用机器码。
这个方案相比传统的tracing JIT,少了很多运行时信息收集和优化决策,但它明确瞄准了一个目标:在不需要完全优化的情况下,把执行路径的开销降下来。
这里注意一个词:不需要完全优化。这意味着3.13的JIT不是一个能把你所有Python代码变异步执行的魔法引擎。它的最大收益来自减少微操作调度循环的开销——每次执行一个微操作时,解释器都要做安全检查、跳转判断、栈深度维护等,而编译成机器码后,这些信息变成了指令里的一部分,直接用寄存器传递,省掉了很多内存交互。
3.3 JIT的实际加速效果与适用边界
关于JIT的实际效果,官方给出的数据比较克制,开发者在多个benchmark上观察到的额外提升大致在10%-20%之间,个别热点函数更高,但不是所有负载都能拿到这个收益。我在自己的项目里测试的结果也是这个数量级:纯函数计算密集的模块,开启JIT后有明显感知;而主要在做I/O、字符串处理和外部服务调用的模块,几乎无感。
要想用上JIT,需要在编译CPython时显式开启:./configure --enable-experimental-jit。注意它是实验性功能,意味着:
- 在不同平台上可能崩溃或行为异常;
- 覆盖率工具、调试工具、
sys.settrace这类运行时检测工具和JIT可能冲突; - 官方不会对JIT模式的兼容性做默认保证。
如果你的项目在生产环境跑,我的建议是:先观望,不要因为性能诱惑把实验性开关直接开进线上。想提前体验的话,可以在持续集成环境里单独建一个JIT构建,专门跑核心接口的性能回归测试,用来观察它对你们代码形态到底有没有正向作用。
如果你对JIT内部感兴趣,可以看看CPython源码里的Tools/cases_generator,3.13的微操作生成器,能直接看到每个uop如何被拆解和生成。还有Include/internal/pycore_jit.h,里面涉及JIT的入口和模板管理。这两个文件是理解整套实现的门户,比看官方博客里的示意图要来得实在。
4. 无GIL构建(PEP 703):真正改变多核并行能力的版本
4.1 GIL到底卡在哪,free-threaded又是怎么绕开的
如果说前面讨论的是“让单线程代码跑得更快”,那PEP 703带来的free-threaded构建(无GIL构建)就是在解决“让多线程代码真正并行”的问题。
长久以来,CPython的多线程有一个矛盾:线程切换本身是操作系统层面的,但解释器执行字节码时,一次只有一个线程能持有GIL。I/O密集任务在等待外部响应时会释放GIL,所以多线程在这类场景下仍有意义;但纯CPU密集任务,比如数值计算、加解密、大量循环,多线程根本没法利用多核。
3.13的free-threaded构建从根上把GIL去掉了。但这绝不是“删一个锁”那么简单,因为整个CPython的内存管理、引用计数、垃圾回收都默认自己是“全局唯一执行者”去写的。去掉GIL之后,两个线程可能同时操作同一个对象、同时修改同一个引用计数,这就会出大问题。
官方技术方案包含几个核心机制:偏向引用计数(biased reference counting)和延迟引用计数(deferred reference counting),以及对象级细粒度锁和大量原子操作。可以简单理解为:不再一把全局大锁管所有人,而是给每个人发了一把小锁,谁用谁锁。这个设计让不同线程可以同时执行Python字节码、同时操作不同对象,只在真正共享同一资源时才有锁竞争。
代价是有的:free-threaded构建在单线程场景下通常比标准GIL构建慢5%-10%,这是为细粒度锁和引用计数保护付出的额外开销。所以PEP 703解决的不是“单线程快”,而是“多核能被用起来”。
4.2 为什么“无GIL”不直接等于“更快”
这一点容易被吹过头,我说说实际项目里的观察。
如果你的目标是让单个计算过程更快,无GIL帮不上忙,反而会因为锁和原子操作的开销拖慢。无GIL的收益场景是:程序可以拆成多个独立计算任务,分别交给多个线程,叠加多核吞吐。比如一个数据管道里有4个相互独立的转换步骤,标准GIL构建下4个线程串行交替执行,无GIL构建下4个线程同时跑满4个核,整体吞吐量就有机会成倍增长。
而且,free-threaded的兼容性是个大问题。你去数一下常用的第三方库,凡是包含C扩展的,很多默认构建的二进制扩展默认假设GIL存在,比如直接操作PyGILState_Ensure、不加锁地修改共享状态。这类扩展在free-threaded构建下要么拒绝加载,要么数据竞争导致崩溃。numpy、pandas这些库已经在适配,但我写这篇文章的时候,很多中小编译扩展仍然没有官方free-threaded版本。
所以,无GIL构建更适合目标明确、依赖可控的项目。如果你的项目就是纯Python代码,或者所有C扩展都明确支持free-threaded,那可以尝试迁移;否则,为了无GIL去修第三方库,工程量会远超收益。
4.3 怎么体验free-threaded版本
想体验的话,最简单的方式是下载官方的free-threaded安装包。在Windows上,安装器里可以选择安装带t后缀的解释器,比如python3.13t.exe;在Linux下通常需要自己用--disable-gil配置编译,或者找社区仓库里预构建的free-threaded版本。
启动后可以验证一下GIL状态。我记得在free-threaded构建里,可以通过sysconfig配置或者运行时的标记来确认,比如带t后缀的可执行文件基本可以确定是无GIL构建。然后写一个简单的多线程CPU密集测试,在两个不同构建下对比运行时间,就能直观感受差异。
必须再强调一次:free-threaded在3.13里是实验性功能,官方不会保证它达到生产级稳定。你要是不想折腾,可以继续用标准构建,等3.14、3.15把free-threaded打磨得更成熟再上。性能提升讲的是长期价值,不是“这周必须用上”。
5. 藏在release notes角落里的优化细节
5.1 整数除法、列表内建操作的专项加速
大版本的功能号角吹完之后,release notes里还有很多小优化,单独看每个都“就这?”,合在一起却能让某些特定代码快上不少。
第一个是整数除法的优化。Python的长整数除法在3.13里优化了内部实现,尤其在做a // b和a % b这类运算时,对除数为常量的场景有更高效率。这个优化对大部分业务代码影响很小,但对数值计算、量化策略回测这类大量使用整数除法的场景,能省下一些时间。不要指望它带来量级变化,但积少成多。
第二个是列表操作的专项加速。前面在自适应解释器部分提到了list.append和list.pop的特化,3.13在C层面也改善了一些与列表内存增长相关的路径。比如循环中高频append时,多次数组扩容带来的realloc开销,在3.13里减少得比较明显。我自己跑过一个构造一万个元素列表的微型基准,相对3.12提升了15%上下。
第三个是布尔值比较的优化。两个布尔值用<、<=等比较运算符进行排序或比较时,会直接按0和1整数比较来处理,省去了走通用比较协议的开销。这类细节很不起眼,但排序布尔列表、状态机切换这类代码里,它的作用能慢慢累积。
5.2 垃圾回收和交互体验相关的延迟优化
还有一个容易被忽略但实际很重要的点,是增量垃圾回收的改进。
CPython的垃圾回收之前有比较明显的“stop-the-world”阶段,当内存里的对象数量很多、引用关系很复杂时,一次完整的GC遍历会造成几十毫秒甚至更长的停顿。这在Web服务里表现为偶尔的响应尖刺,在交互式应用里表现为界面卡顿。
3.13对GC做了增量化的改进,把原本集中进行的标记阶段拆成多个小阶段,穿插在字节码执行之间完成。这样每次停顿的时间被摊薄,大大减少了“一下子卡住”的情况。从吞吐量上看,增量GC不一定比原来快,但延迟抖动明显改善。对跑Web接口、交互式脚本的人而言,这种体感上的流畅比纯数字提升更重要。
另外,3.13还新增了random.binomialvariate()函数,服务于二项分布采样。这项新特性与性能没有直接关系,但如果你做蒙特卡洛模拟或统计抽样,官方实现会比自己用random.random()绕弯子快不少,也算一种性能替代方案。
还有一点值得提到的,是3.13在Windows安装包里默认对JIT做了一些预置。我不是说Windows版就等于JIT全开,而是建议你在不同平台上多留意官方安装包的说明。有的是编译阶段开启,有的是运行时环境变量,具体情况建议直接查阅你所用构建的官方文档。
6. 实测与升级建议:怎么判断自己的项目该不该上3.13
6.1 用pyperformance做一组可复现的对比
前面讲了那么多技术细节,落到实践上,最好的办法是自己跑一次可复现的benchmark。官方工具是pyperformance,用法很简单:
bash复制# 在同一个环境里分别安装3.12和3.13
pip install pyperformance
# 运行基准测试
python -m pyperformance run -o py312.json
python -m pyperformance run -o py313.json
# 对比结果
python -m pyperformance compare py312.json py313.json
跑完会输出一张表,列出每个基准的相对变化。注意跑的时候尽量让机器空闲,关闭其他高负载进程,多次跑取中位数,避免噪声影响判断。
但pyperformance的结果只反映“CPython在通用负载上的表现”,不代表你的项目。所以真正的升级判断要分两步:第一步看官方benchmark的总体方向,第二步拿自己项目里的核心模块做对比。
我自己测试的做法是,写一个针对项目特征的脚本,比如一个大循环里交替做列表操作、字典读写、整数运算,再模拟一些函数调用密集型场景,分别在3.12和3.13上跑十次,取中位数对比。这样做出来的结论可以直接回答“我该不该升级”。
6.2 升级前必须检查的兼容性清单
如果决定升级,下面这个清单可以帮你少踩坑。经验之谈,按顺序检查:
第一,C扩展的wheel是否支持3.13。二进制扩展包需要官方发布适配cp313 ABI的wheel。常见的numpy、pandas、scipy、lxml、pydantic-core在3.13发布初期已经跟上,但一些不那么主流的库可能会滞后。在虚拟环境里执行pip install -r requirements.txt,如果某个包下载的是源码包并尝试本地编译,就要重点关注,很可能是还没有cp313 wheel。
第二,依赖GIL的扩展库是否打算用free-threaded。如果不碰free-threaded构建,这条可以跳过。如果打算用,需要到每个扩展库的issue页面确认free-threaded兼容状态。
第三,运行时代码分析工具。如果你的项目重度依赖sys.settrace、sys.setprofile、调试器、性能分析器,要注意这些工具在tier 2解释器和JIT开启时的行为可能变化。官方代码中没有完全放弃对这些工具的支持,但某些特化路径可能与追踪冲突,导致数据不准确或需要额外配置。
第四,业务代码里是否有依赖字节码结构的操作。比如自定义的字节码解析工具、或者直接在frame.f_lasti上做逻辑的库。3.13把tier 2和JIT引入后,代码对象和帧的状态比之前复杂,这类操作风险很大,需要单独验证。
第五,内存占用是否敏感。3.13的内联缓存和tier 2微操作序列会带来额外的内存开销,比重不大,但对内存受限的嵌入式环境或大规模并发worker场景,可能需要重新评估。
我推荐的做法是,建一个独立虚拟环境,用3.13解释器跑完整的项目测试套件,再跑一遍生产环境的代表性任务,对比正确性和资源占用。测试全绿、性能符合预期再考虑迁移线上。
从3.12升级到3.13的迁移成本不算高,绝大多数纯Python代码可以无缝运行。如果你还在3.10或更老的版本,那这次升级的收益会更明显,因为中间跨越了多个大版本的优化成果。但要注意,跨大版本升级时,第三方依赖的版本要求也会变化,先升级依赖再升级解释器,顺序不要反。
我在实际使用中最深的体会是:性能优化带来的收益,在“明确知道自己瓶颈在哪”的人手里才有效。如果你还没做过性能分析,不知道自己的代码时间花在哪里,那升级3.13大概率只是“心里觉得快了”。先跑一遍profile,把热点找出来,再对照这篇文章里提到的特化、JIT、无GIL这些特性,判断它们各自能不能帮到你的热点,这个思路比任何一个版本的发布说明都重要。
