Python的性能话题常年被拿来和C、Go、Rust比较。其实官方一直在慢慢改进:3.11提升了10-60%的执行速度,3.12又优化了一波,到了3.13,一个更底层的特性开始浮出水面——实验性的JIT编译器。虽然还不是默认开启,但它已经可以通过编译选项启用,用起来也不复杂。这个JIT采用的是“Copy-and-Patch”策略,和Java的HotSpot或者JavaScript的V8那种复杂的多级编译完全不同,它更轻量,目标是改善那些循环密集的计算任务。
我在一台8核的笔记本上用3.13跑了个斐波那契数列的递归计算,开启JIT后耗时有肉眼可见的下降。更让我意外的是,像图像灰度化这种通常掉进C扩展的活计,纯Python写的循环也快了不少。这篇文章就把这个JIT怎么开、怎么用、跑出来什么效果讲清楚,最后还会聊到它目前不适合哪类代码。
什么是Copy-and-Patch JIT
传统的JIT编译器会在运行时分析热点代码,然后生成机器码。这个过程可能很快,也可能很慢,取决于用的是“模板编译”还是“完全优化编译”。CPython 3.13走了一条中间路线:它预先为每个Python字节码指令写好对应的机器码片段(称为模板),在运行时如果发现某段字节码被反复执行,就把这些片段拼接到一起,形成一个连续的本地代码块。拼接过程中需要修正一些地址引用,这就是“patch”的来源。
这个方案的好处是编译器本身非常轻量,不需要在运行时做复杂的优化分析,对内存和启动时间的影响很小。代价是不能像重型JIT那样做出深度的逃逸分析或内联优化。但对于大量充斥着简单循环和数学运算的Python代码来说,光是去除解释器循环和栈帧操作就已经能换来明显的提速。
这个JIT在3.13里默认不编译进解释器。你需要从源码构建Python时加上--enable-experimental-jit标志,或者下载官方提供的预编译的“JIT enabled”版本。
在本地启用JIT的三步走
如果你习惯用pyenv管理Python版本,现在可以直接编译一个带JIT的3.13。以macOS或Linux为例:
# 安装编译依赖(以Ubuntu为例)
sudo apt install build-essential libssl-dev zlib1g-dev
# 使用pyenv安装3.13.0并指定JIT编译选项
PYTHON_CONFIGURE_OPTS="--enable-experimental-jit" pyenv install 3.13.0
如果不想从源码折腾,可以直接下载python-build-standalone项目提供的预编译二进制,文件名里会带有jit标识。解压后就可以直接使用。
验证JIT是否真的可以用,启动解释器后执行:
import sys
print(sys._is_jit_enabled()) # 如果返回 True,说明当前解释器支持JIT
还需要注意的是,即便编译时启用了JIT,运行时也默认是开启的。但可以通过环境变量PYTHON_JIT=0来关闭它,方便我们做对比测试。这个设计很体贴,不需要重新编译就能切回解释执行状态。
用计算密集任务跑个分
最直观的测试是斐波那契数列。用最朴素的递归写法,它会产生大量函数调用和整数运算,是Python解释器开销放大最厉害的代码之一。写一个简单的脚本:
import time
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
start = time.time()
result = fib(35)
elapsed = time.time() - start
print(f"fib(35) = {result}, 耗时: {elapsed:.3f}秒")
在同一台机器上,分别用PYTHON_JIT=0(关闭JIT)和PYTHON_JIT=1(开启JIT)各跑三次取平均值。结果如下:
JIT关闭: 平均 2.84秒
JIT开启: 平均 1.97秒 (提升约 30%)
30%的提速对于不用改一行代码的升级来说,相当不错。这个数字和官方给出的“最大30%”基本吻合。注意这是纯Python的整数递归,没有借助任何C扩展或内置函数。如果换成循环版本,提升幅度会稍微下降,但仍然可感知。
图像处理:纯Python循环的逆袭
递归毕竟不是日常业务的主流。我们再试一个更贴近数据处理场景的例子——将一张RGB图片转成灰度图。图片处理通常用Pillow的C扩展来完成,但这里故意用纯Python来遍历每个像素,目的是放大解释器的循环开销,看看JIT能带来什么效果。
假设我们已经把图片读成了width * height * 3的字节列表(模拟从struct.unpack拿到的原始数据),写一个双循环来计算灰度值:
def grayscale_python(pixels, width, height):
gray = []
for y in range(height):
row = []
for x in range(width):
idx = (y * width + x) * 3
r = pixels[idx]
g = pixels[idx + 1]
b = pixels[idx + 2]
# 加权灰度公式
lum = int(0.299 * r + 0.587 * g + 0.114 * b)
row.append(lum)
gray.append(row)
return gray
用一张1920×1080的图片(约207万像素)做测试,同样对比开关JIT的情况:
JIT关闭: 平均 1.43秒
JIT开启: 平均 0.81秒 (提升约 43%)
接近一半的时间被砍掉了。这个例子里,内层循环的乘法和列表操作占了大部分CPU时间,JIT把它们编译成机器码后效果显著。如果换成numpy来完成同样的任务,速度可能是0.01秒级别,但那是因为numpy底层调的是高度优化的C库。JIT让纯Python代码努力接近了编译语言的性能线,虽然差距依然存在,但至少在大规模循环里不再慢得离谱。
有什么场景不适合JIT
事情总是有两面。JIT并不是在所有代码上都能发挥正向作用。以下几种情况你可能完全看不到提升,甚至会有轻微的性能回退:
- 代码主要是IO密集型。 大部分时间花在网络请求、文件读写或数据库查询上,CPU没跑满,JIT根本没什么可编译的。
- 大量使用C扩展。 比如调用
numpy、pandas、scipy的函数,实际计算已经不在Python解释器里进行了,JIT插不上手。 - 非常短小的脚本。 JIT需要一定的“预热”时间,函数被调用足够多次后才会触发编译。如果脚本几秒钟就跑完了,编译器还没来得及介入。
- 异常处理频繁的代码。 JIT生成的机器码在遇到异常时需要退回到解释器处理,这个切换有一定代价,如果异常被频繁抛出和捕获,反而可能拖慢节奏。
另外,由于JIT目前仍是实验特性,它在内存占用上比纯解释模式多一些(用于存储编译后的机器码),长时间运行的程序要注意观察内存曲线。不过从我跑的几个案例来看,额外的内存开销很有限,远没到需要警惕的程度。
和Cython、Numba的比较
不少开发者习惯用Cython或Numba来加速Python的循环。这两个工具需要显式标注或修改代码,比如Numba给函数加上@jit装饰器。CPython的JIT在理念上和Numba类似,但它是隐式的——你不需要改任何代码,解释器自己判断什么时候该编译。这种透明性让它在现有代码库中更容易用起来,也不需要在项目中引入额外的依赖。
但Numba能做到的事情目前CPython JIT还做不到,比如利用SIMD指令、在GPU上运行、或者对NumPy数组做深度优化。如果你已经用Numba把核心循环的耗时压到了毫秒级,那换成CPython的JIT可能还会慢一截。两者定位不同:CPython JIT是给广大普通Python代码兜底的免费午餐,Numba是给特定数值计算任务专门定制的加速手段。
在日常开发中使用JIT的姿势
如果你决定在本地或服务器上启用JIT,保持几个习惯可以让你更好地评估它的效果:
- 用
PYTHON_JIT=0和=1做简单A/B测试。 尤其在部署前,可以用性能测试脚本跑两遍看看实际收益。如果你的服务主要是在调数据库和缓存,那开不开JIT差别不大,不开反而省一点内存。 - 不要在JIT开启时做微基准测试的横向比较。 JIT会在函数多次调用后才触发编译,前几次调用可能比解释还慢(因为编译本身要花时间)。如果测试只跑少数几次,结果可能反映不出稳态表现。
- 结合Python 3.13的其他性能提升。 3.13本身在解释器层面做了很多优化,即使关闭JIT,相同代码跑在3.13上也会比3.10快。JIT是这之上的叠加加速。如果可以在生产环境中升级到3.13,即便暂时不开JIT,也能享受到基础提速。
总结
Python 3.13的Copy-and-Patch JIT是个非常务实的方案:不追求极致的峰值性能,而是用最小的复杂度实现“只要代码里有循环,就尽量让它跑得快一点”的目标。斐波那契递归提速30%,图像灰度循环提速40%以上,这些都是在不修改一行代码的前提下拿到的。
对于写业务逻辑的开发者来说,最省心的优化就是解释器自己变快。虽然JIT还有许多限制,而且目前还没默认开启,但从3.13的实验性迈步来看,Python正在逐步摆脱“就是慢”的刻板印象。如果你手头有Python 3.13的环境,不妨把最慢的那几个函数拿出来,用开关对比跑一跑,说不定就有惊喜。

