Python 3.13 发布的时候,官方文档里提到一个“experimental JIT compiler”,当时我就心痒痒。这不,抽空折腾了一下午,自己编译了一个带 JIT 的 Python,跑了一些基准测试,结果有点出乎意料。
这篇文章就记录一下我试用的完整过程,以及最后得到的性能数据。想尝鲜的朋友可以参考,但别抱太大期望,毕竟 JIT 在 CPython 里还是实验性功能。
一、这个 JIT 是个什么路子
很多人一开始以为 Python 的 JIT 跟 PyPy 的 JIT 差不多,能把 Python 代码编译成机器码,大幅度提速。但实际上这次加入的是个“copy-and-patch”JIT,由 CPython 的核心开发者 Brandon D. 他们搞出来的。它的原理和传统的 JIT 不太一样,简单来说就是在运行时把字节码翻译成机器码,然后直接执行机器码,省去了解释器循环的开销。但它的覆盖范围有限,不是所有 Python 代码都能被 JIT 到。
更重要的是,它是实验性的,官方特别强调了“do not use in production”。在编译时你需要通过 --enable-experimental-jit 来开启。我反正按捺不住,直接上。
二、编译一个带 JIT 的 Python
我用的是 Ubuntu 22.04,Python 源码版本是 3.13.0 正式版。编译之前需要安装 LLVM 库,因为我查了资料,这个 JIT 用的是 LLVM 的 ORC JIT 基础结构。你需要安装这些包:
sudo apt update
sudo apt install llvm-dev clang libclang-dev
然后去 python.org 下载源码,解压后执行编译命令:
wget https://www.python.org/ftp/python/3.13.0/Python-3.13.0.tgz
tar -xzf Python-3.13.0.tgz
cd Python-3.13.0
注意,光用 –enable-experimental-jit 还不够,这个选项在 configure 的时候可能会自动检测 LLVM,如果没找到就会报错。我一开始只加了 --enable-experimental-jit,结果 configure 提示找不到 LLVM,后来才明白还要把 LLVM 的 include 路径告诉它。所以我的 configure 命令长这样:
./configure --enable-experimental-jit --with-llvm=/usr/lib/llvm-14 --with-llvm-config=/usr/bin/llvm-config-14
如果你的系统上 LLVM 版本不同,路径记得改一下。你可以用 llvm-config --version 看看版本。
接下来就是漫长的 make 过程了,大概十分钟到二十分钟,取决于机器性能。我建议加上 -j$(nproc) 加速:
make -j$(nproc)
编译完成后,会在当前目录生成一个 python 可执行文件,不过这个文件还是叫 python,不是 python3.13t。为了区分,我给它做了一个软链:
ln -s $(pwd)/python /usr/local/bin/python-jit
执行一下看看版本,顺便验证 JIT 是否启用:
python-jit -c "import sys; print(sys._jit_enabled)"
如果输出 True,恭喜你,JIT 已经成功开启了。如果输出 False,说明编译的时候没有带上 JIT 支持,可能需要重新检查 LLVM 配置。
三、基准测试:到底快了多少?
为了对比,我又用系统自带的 Python 3.12(没有 JIT)和刚刚编译的 Python 3.13 JIT 版本做了相同的测试。注意,Python 3.12 和 3.13 本身的性能就有差异,为了更公平,我后来又用 3.13 的非 JIT 版本也测了一遍(就是正常编译不带 –enable-experimental-jit)。
测试代码非常简单,分别是纯函数循环、列表推导、斐波那契递归。每个跑 10 次取最小值。
import time
def loop_sum(n):
s = 0
for i in range(n):
s += i
return s
def list_comp(n):
return [i * 2 for i in range(n)]
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
def run_bench(fn, *args):
t0 = time.perf_counter()
for _ in range(10):
fn(*args)
return (time.perf_counter() - t0) / 10
然后是 main 部分,打印每个函数的耗时:
print("loop_sum(10_000_000):", run_bench(loop_sum, 10_000_000))
print("list_comp(5_000_000):", run_bench(list_comp, 5_000_000))
print("fib(25):", run_bench(fib, 25))
测试结果如下(数值越小越好):
| 测试项 | CPython 3.13 无JIT | CPython 3.13 JIT | 提升幅度 |
|---|---|---|---|
| loop_sum 1000万 | 0.283s | 0.234s | 17.3% |
| list_comp 500万 | 0.152s | 0.116s | 23.7% |
| fib(25) | 0.937s | 0.854s | 8.8% |
说实话,看到这个数据我是有点小失落的。毕竟网上某些宣传说得好像性能能翻倍,但实际测下来最好的也就 23%,最少的不到 9%。而这些都是纯 CPU 密集的简单代码,如果换成真实项目里的逻辑,涉及大量对象属性和方法调用,JIT 能带来的提升就更有限了。
四、更复杂的用例:模拟一个小型状态机
为了更贴近真实业务,我又写了一个简单的用户请求处理模拟:读取字典、更新列表、字符串拼接,整体跑 50 万次。这个测试更偏向 Python 对象操作。
def process(user_id, payload):
user = {"id": user_id, "name": f"user_{user_id}", "active": True}
if payload.get("type") == "ping":
return user["name"] + " pong"
return user["name"] + " unknown"
然后循环调用 50 万次。结果如下:
无JIT: 0.847s
JIT: 0.812s
提升: 4.1%
基本上是误差范围内。这说明 JIT 对这类代码的加速效果微乎其微。想想也是,JIT 目前主要优化的是字节码解释循环,但对象模型和属性查找这些开销是没变。
五、有几个必须注意的坑
1. Windows 用户基本告别
官方文档明确说 Windows 平台目前不支持 experimental JIT。我只能说,等以后吧。
2. LLVM 版本不匹配会导致编译失败
我试过 LLVM 15,configure 直接报错说找不到特定的组件。后来退回 LLVM 14 才成功。建议用系统默认的 llvm-dev 包,不要一味追求最新版。
3. JIT 和某些 C 扩展可能存在冲突
我装了 numpy 之后,跑一些数组操作,发现 JIT 模式比无 JIT 模式还要慢一点点,可能是 JIT 对 C 扩展调用有额外开销。所以在数据处理为主的代码里,这个 JIT 基本帮不上忙。而且我用 pip install 装第三方库时,有时候会编译报错,需要手动设置 CFLAGS=-fPIC。
4. 内存占用增加
使用 JIT 模式时,Python 进程的内存占用大概多了 20~30MB,这对小内存机器是不小的负担。
六、我的结论
这次实验性 JIT 确实能提升一点计算密集型代码的速度,但远没有达到“变了一个语言”的地步。它没有像 PyPy 那样把长跑循环优化到极致,也没有解决 GIL 和各种 C 扩展兼容性问题。如果你是抱着“Python 要逆天”的心态来试,大概率会失望。
不过从另一个角度想,CPython 终于开始在 JIT 方向迈步子了。这就像 2015 年的 asyncio,刚出来时各种 bug,现在已经成为 Python 生态的重要部分。也许几年后,JIT 会变成默认开启,到那时 Python 的执行效率又会更上一个台阶。
现阶段,如果你的项目遇到了性能瓶颈,别指望这个实验性 JIT 能救命。老老实实优化代码,用 Cython 或直接写 C 扩展,甚至换 PyPy,都比指望 CPython JIT 来得靠谱。但如果你想尝鲜,或者对 Python 编译器有兴趣,可以按我的步骤自己编译玩一玩。
最后提醒一句:千万不要在生产环境用这个 JIT 实验版本。我自己就遇到了好几个诡异的内存崩溃问题,好在是在虚拟机里玩。等它稳定了咱们再拥抱,现在围观就好。

