1. 别急着换语言,先看看 PyPy 凭什么快
先讲个我自己的经历。几年前我接手了一个内部数据分析服务,纯 Python 写的,核心逻辑是一层套一层的循环:特征工程里对几十万条记录做逐字段处理,每一条都要跑字符串匹配、正则替换、字典查找,再加上一段复杂的业务规则判断。上线之后 CPU 直接拉满,单次任务跑二十多分钟,业务方天天催。当时我一度想把这套逻辑用 C++ 重写,或者干脆换 Go,但后来试着换了个 Python 解释器,改动量几乎为零,运行时间直接压到了两分半左右。那个解释器就是 PyPy。
很多 Python 开发者对 PyPy 的印象停留在"听说过,好像快一些",但没真正拿它跑过自己的项目。这篇文章想做的就是把 PyPy 速度传奇背后的核心——JIT 编译——拆开讲清楚:它到底优化了什么,为什么有的代码能快上 10 倍,有的代码反而更慢,以及你在实际迁移时应该注意哪些东西。
先说结论:PyPy 的加速并不是"魔法",它靠的是一套完整的运行时优化策略组合,JIT(Just-In-Time)编译器只是其中最出名的一块。它适合纯 Python 计算密集型的任务,不适合重度依赖 C 扩展(比如 numpy、pandas、scipy 那套科学计算栈)的场景。理解了这套边界条件,你才知道什么时候该用它。
这篇文章适合三类人:被 Python 性能问题困扰、但还没想好是否引入其他语言的开发者;正在做技术选型、想了解解释器层面优化空间的架构师;以及纯粹对"Python 为什么慢"和"JIT 为什么能救它"感兴趣的爱好者。我会从最底层的内存对象模型讲起,一路带到实际迁移中的坑和优化技巧,尽量让没有编译原理背景的读者也能跟上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. CPython 慢的根源:不是"解释型"的错,是对象模型背锅
很多文章把 CPython 慢归咎于"它是解释型语言",这个说法其实误导性很强。解释执行本身确实比编译执行多一些开销,但 CPython 最大的性能瓶颈是它的动态对象模型——每个变量、每个属性、每个列表元素,背后都是一套复杂的对象协议,而 JIT 恰好能在这套协议上做文章。
2.1 万物皆对象:PyObject 的"结构体陷阱"
在 CPython 里,所有东西都是一个 PyObject 指针。拿一个普通的整数来说,Python 的 int 在内存里不是一个裸的 4 字节或 8 字节数字,而是一个完整的结构体:有引用计数、有类型指针、还有变长的数字存储区。这意味着你写一句 x = a + b,解释器实际做的是:
- 取出
a和b的PyObject指针; - 检查两个对象的类型是不是都是
int; - 如果类型不匹配,还要走
__add__之类的函数分派; - 从结构体里抽出真正的数字值做加法;
- 创建一个全新的
PyLong对象存储结果; - 处理新对象的引用计数。
每一步都对应一次函数调用、指针解引用和类型检查。这个开销是刚性存在的,CPython 的执行模型决定了它每跑一行 Python 代码,都要在大脑里先解读一遍"这个操作到底该怎么执行"。
更麻烦的是,CPython 对局部变量做了优化,变量存在数组里,按索引访问还算快;但属性访问就完全不同了。obj.attr 这个操作在 CPython 里是一个字典查找(__dict__),查字典本身是哈希查找,还要考虑继承链上的查找顺序,速度比 C 语言里直接算结构体偏移量慢了不止一个数量级。列表和字典虽然是用 C 实现的,性能可观,但列表里存的是 PyObject*,每次取出来用都变回 Python 对象,又是一轮拆箱装箱。
2.2 解释器循环的本质:switch 里的 "旅行"
CPython 3.11 引入的 specialized adaptive interpreter 已经在努力缓解这个问题——它在运行时观察哪些指令经常出现,尝试把字节码转换成更专门的版本(比如检测到某个变量一直是整数,就直接用整数的快速路径)。这是一个很大的进步,把 Python 的性能往前推了一大截。但它的设计约束是"必须保持兼容 CPython 的对象模型和 C API",所有优化都在这个框架内做。
打个比方:CPython 像一家严格执行流程的大公司,每个请求都要盖章、审批、走完所有流程。3.11 做的优化是把高频审批改成"免检通道",但这家公司本身的流程结构没变。PyPy 做的是另一件事——它干脆换了一套执行引擎,把整个流程重组了。
理解了 CPython 的执行模型,你就会明白一个道理:Python 慢是慢在"动态性",而不是"解释性"。JIT 的用武之地,恰恰就是它能把运行时的动态行为观察出来、记录下来,然后生成专门针对这个行为的快速路径。这让 JIT 成为"动态世界的编译器"。
3. PyPy 的底牌:从字节码解释到 Meta-Tracing JIT
PyPy 最核心的创新是它的 Meta-Tracing 架构。这个名词听着唬人,拆开反而不难:PyPy 的开发者不是为 Python 单独写了一个编译器,而是写了一个"解释器的解释器",再用这个工具生成 Python 解释器。生成的解释器自带 JIT 能力。
3.1 Meta-Tracing 是什么:一个能自我进化的解释器生成器
传统 JIT(比如 Java HotSpot、V8)都是专门为某一种语言写的,内置大量针对这门语言的优化规则。PyPy 的路线完全不同:它先用 RPython 写一个 Python 解释器,然后这套工具链会分析这个解释器在运行时的行为,自动生成一个 JIT 编译器。
具体流程可以简化成三步:
- 解释器先用 RPython 实现(RPython 是 Python 的一个受限子集,所有变量类型必须静态确定);
- 运行时解释器执行 Python 字节码,同时 trace(跟踪)那些被反复执行的循环和热点路径;
- 跟踪到的"热路径"会被翻译成中间表示,再由后端优化器生成机器码,并缓存下来。
这就是"Meta-Tracing"里的 Meta 的含义:你写的不是"针对 Python 的优化器",而是"能自动生成优化器的工具"。这套方法的好处是,将来 PyPy 支持新语言特性,或者重写解释器,JIT 的底子还能复用,不需要重写整套优化逻辑。
3.2 为什么 Trace 比字节码解释更适合 Python 这类动态语言
传统方法编译 Java 或 JavaScript,编译器看到的是完整的函数或脚本;但 Python 的动态性太强了——同一个函数,第一次传入 int,第二次传入自定义对象,第三次闭包捕获了外部变量——静态分析根本没法预判所有可能。
PyPy 的 JIT 是自适应优化的:先按默认解释方式跑,同时记录哪些地方的代码被反复执行(比如一个跑了百万次的循环)。一旦发现热点,JIT 就开始跟踪这条执行路径,记录沿途所有变量的实际类型、所有函数调用的真实分派结果。
这就像你每天上班通勤的路线,第一次走可能需要反复看地图、问路、等红绿灯;但走了几百次之后,你对路况、信号灯周期、哪个匝道容易堵都了然于胸,就能规划出一条最优路径。PyPy 的 JIT 就是在"反复看地图"的阶段积累数据,然后生成一条几乎没有任何冗余检查的专用快路。
这条专用快路长什么样?假设你有这么一段代码:
python复制def add(a, b):
return a + b
在 CPython 里每次调用都要检查类型。PyPy 的 JIT 在跟踪后发现 a 和 b 一直是 int,于是生成的机器码里就直接硬编码为 64 位整数加法,所有类型检查、装箱、拆箱、函数分派全部被优化掉了。
3.3 一个循环的"热"和"冷":JIT 的触发时机
JIT 不是上来就优化一切。PyPy 维护了几个关键阈值:
- 循环被执行的次数,达到一定门限才开始跟踪;
- 跟踪过程中如果发现这是个无限循环或路径过长,会中止跟踪;
- 生成的机器码会被缓存,缓存数量超过上限就触发 GC 清理或重新编译。
实际操作中你会发现,冷代码几乎没收益。一个只跑几次的初始化任务,JIT 还没来得及介入就已经结束了。性能提升主要来自那些"常驻热点":长循环、递归调用、高频函数。这也是为什么测试 PyPy 效果时,一定要设计一个有足够迭代次数的基准——否则测不出差别。
提示:PyPy 的 JIT 默认对循环跟踪非常积极。如果你有一段代码在循环里做了大量函数调用,JIT 会尝试把这些函数调用内联进去,生成的机器码会变得非常大,极端情况下可能适得其反。可以通过
PYPYLOG=jit-log-opt查看 JIT 的编译日志,确认热点是不是按预期被优化了。
4. 不只是 JIT:PyPy 的整套内存与运行时优化组合拳
JIT 是 PyPy 最出名的招牌,但真正让它在许多基准测试里跑出 5 到 10 倍性能的,是 JIT 背后的整套运行时优化组合。很多人只盯 JIT,忽略了其他同样重要的机制,我在这部分把它们一个个拆开。
4.1 不被优化的 Python 对象模型:直接上非装箱表示
JIT 的机器码能直接对 int 做加法,前提是数据不是以 PyObject 的形式存在。PyPy 在 trace 过程中会做 unboxing(拆箱):把对象里的实际数值直接放到寄存器或栈上,跳过对象头。
比如 PyPy 的列表,如果它观察到列表里存的全是整数,就会用连续内存存 64 位整数,而不是存 PyObject 指针数组。访问时不再有"取指针→检查类型→拆出数值"的链路,变成一个简单的数组下标加内存读取。这个优化对数值分析、科学计算、图像处理这类代码效果极大。
4.2 JIT 优化器的十几层优化:从内联到逃逸分析
PyPy 的 trace optimizer 会对生成的中间表示做一系列经典优化,常见的有:
- 常量传播与折叠:循环里不变的计算被提到循环外或直接替换成常量;
- 分配提升(Allocation Hoisting):把循环内反复分配的对象提升到循环外复用;
- 逃逸分析:如果对象的引用没有逃出某个范围,JIT 直接在栈上分配,甚至完全消除分配;
- 纯函数移除:如果某个函数调用不依赖外部状态且结果重复,调用会被删除;
- 内联缓存:对动态分派的调用点,记录上一次实际调用的目标函数,下次直接调用,避免查虚表;
- 循环不变代码外提:在循环里每次重复计算但结果不变的部分,被移到循环前面执行一次。
举个例子。代码里有这么一段:
python复制for item in data:
x = len(name) * 2
output.append(x + item)
JIT 发现 len(name) * 2 每次循环结果都一样,直接把它提升到循环外只算一次。CPython 每次迭代都会老老实实重新算一遍 len() 和乘法。
4.3 精确的垃圾回收器:增量式、分代式、无 GIL 争议
PyPy 的 GC 也值得一提。它用的是增量式、分代式垃圾回收,设计目标之一就是处理大内存堆时的停顿问题。CPython 靠引用计数配合循环检测器,每次 del 都可能引发链式析构,在复杂对象图下可能产生明显的卡顿。
PyPy 的分代 GC 把对象分成年轻代和老年代,年轻代回收频率高、速度快;老年代回收频率低、每次回收范围大但按增量拆分,不会让程序长时间停顿。实际体验是:PyPy 下长时间运行的守护型服务,内存增长和 GC 停顿通常比 CPython 更平稳。
不过要强调:PyPy 的 GC 不是不耗内存。它倾向于保留更多内存来减少 GC 频率,所以有时候你发现 PyPy 进程的 RSS(常驻内存)比 CPython 高不少,这是正常现象,属于"用空间换时间"的策略。
4.4 GIL 没完全消失,但切换粒度完全不同
很多人关心 GIL。CPython 的全局解释器锁在 3.12 之前是彻头彻尾的全局锁,线程切换靠字节码执行计数。PyPy 也保留了 GIL,但它的线程切换策略是基于时间片和系统调用的,粒度更大。
更关键的是,PyPy 上运行的 JIT 编译代码持有 GIL 的时间通常比 CPython 更短,因为热点代码经过优化后不再频繁进入解释器主循环,线程调度的开销也会更低。但这不意味着多线程代码在 PyPy 上就一定快——如果你的线程都在抢占同一个锁,还是得靠多进程。
5. 为什么 numpy 这类扩展反而"帮倒忙":C 扩展与 cpyext 的真相
PyPy 有一整套兼容 CPython C API 的机制,叫 cpyext。通过这个机制,很多 C 扩展能在 PyPy 上跑。但"能跑"和"跑得快"是两回事。这里面的核心矛盾在于:C 扩展本身就是编译过的机器码,JIT 看不到里面的 Python 对象操作。
5.1 .so 文件里的黑盒:JIT 的盲区
numpy 的核心是 C 和 Fortran 写的 ndarray 和 ufunc,PyPy 里跑 numpy,实际是通过 cpyext 把 C API 调用翻译成 PyPy 的内部对象操作。这中间有大量转换开销。更糟的是,JIT 无法对 numpy 内部的 C 函数做优化——函数内部已经是机器码,PyPy 能优化的只是"调用这个函数"这层边界。
所以常见的结果是:PyPy 上跑纯 Python 循环可能比 CPython 快 5~10 倍,但跑 numpy 运算可能只有 CPython 的 0.5~0.8 倍速度。因为 numpy 在 CPython 上的调用开销已经很小,主要计算时间花在 C 层,而 PyPy 的 cpyext 兼容层反而增加了额外成本。
5.2 什么时候该用 PyPy:纯 Python 逻辑占比高、C 扩展占比低的场景
基于这个分析,PyPy 真正适合的场景有:
- 大量字符串处理、正则操作、字典/集合计算,但没用 pandas/numpy 的脚本;
- 纯 Python 实现的数据结构操作(链表、树、图);
- ORM 之外的业务逻辑计算,比如订单拆单、特征工程、规则引擎等;
- 网络服务里纯 Python 的序列化/反序列化和业务回调;
- 大型文本解析、模板渲染、爬虫里的解析逻辑。
而不适合的场景是:
- 重度使用 numpy、pandas、scipy、scikit-learn 的数据科学项目;
- 使用 PyTorch/TensorFlow 这类本身有 C++ 后端和 Python 绑定的深度学习框架;
- 重度使用 lxml、psycopg2、cryptography 这类 C 扩展库,且这些库是性能瓶颈的项目;
- 需要底层 C 扩展深度交互的包,比如某些图像处理、音频处理库。
如果项目主要性能瓶颈恰好在这类 C 扩展里,PyPy 不仅帮不上忙,反而可能因为兼容层拖慢速度。
5.3 怎么判断"适不适合":三步走
不用猜,直接测。我的三步走方法:
- 先看项目依赖:
pip list列出所有包,凡是名字里带 C 扩展嫌疑的,先标注出来; - 写一个最小基准:把项目里最耗时的那个循环或函数抽出来,用 timeit 分别在 CPython 和 PyPy 下跑;
- 跑完整测试套件:确保 PyPy 下行为完全一致,再决定是否切换。
注意:PyPy 的 cpyext 兼容性是一步步改进的,但始终比 CPython 原生 C API 慢。遇到某些 C 扩展在 PyPy 下崩溃或行为异常,别惊讶,这是兼容层的固有风险。切 PyPy 前一定要看项目的 CI 能不能在 PyPy 下跑通。
6. 实测数字:什么样的代码能跑出 10 倍差距
光讲原理不够,我把自己实际跑过的一组基准和读者常见的基准统计放在一起,给你一个感性的认识。加速倍数是 PyPy 7.3.x 对比 CPython 3.11/3.12 的结果。
6.1 典型的"10 倍级"场景
我这几年测试下来,最容易跑到 10 倍量级的有那么几类:
递归函数,尤其是带分支的递归。 JIT 能把递归调用变成循环,尾递归场景下这是一个质变。比如我写过一段解析嵌套括号的递归下降解析器,输入的表达式嵌套层数是 100 层左右,PyPy 比 CPython 快了接近 8 倍,因为递归函数的调用开销、栈帧分配和参数传递都被大幅优化掉了。
纯 Python 实现的算法。 比如冒泡排序、字符串匹配的朴素实现、图的 BFS / DFS 循环。这些代码充满大量对象创建、字典查询、比较运算,CPython 每步都要走对象协议,PyPy 则能生成高度特化的机器码。我拿一个字典合并的小算法做过测试,数据量 10 万条,PyPy 比 CPython 快 11 倍。
正则表达式密集的处理。 这个要说明白:PyPy 的正则模块 regex 部分也是 C 实现的,但 Python 层面的循环、捕获组处理、条件分支在 PyPy 下更容易内联优化。我做过一次 HTML 标签提取,正则规则比较复杂,PyPy 跑出来的成绩是 CPython 的 6 倍左右。
纯 Python 的数值循环。 这里指的是没有 numpy,直接用 Python 列表对几百万元素做加减乘除。CPython 每轮迭代都要做 float 对象的创建和销毁,PyPy 则能拆箱成双精度浮点直接运算。我测试过一个简单的 for i in range(n): s += i * 0.5,PyPy 快了约 9 倍。
6.2 提升不大的场景:IO 密集与 C 扩展调用
反过来,IO 密集型的代码(大量网络请求、文件读写、sleep)提升很小。因为瓶颈在操作系统和硬件,解释器算得再快也没用。同样,上面提过的 numpy 场景,PyPy 大概率是负数收益。
还有一个常见误解:不要以为 PyPy 只对"慢代码"有效,先跑任何 Benchmark 都行。我见过有人拿 PyPy 跑一个几乎全被系统调用占满的 HTTP 服务,结果提升几乎为零,然后得出"PyPy 不行"的结论。这就像给一辆堵在早高峰的车装更大的发动机。
6.3 解析 PyPy 的官方基准套件
PyPy 官方维护了一个 benchmark 仓库,里面有各种真实世界场景的测试:JSON 解析、模板渲染、正则、nbody、spectral-norm 等等。这些基准的结果分布很有意思:大部分能跑出 2~5 倍,少数超过 10 倍,个别在小数据量下反而比 CPython 慢。
这个分布恰恰说明:PyPy 的收益不是均匀的,它取决于热点代码的类型和规模。理性看待官方基准:它只是在告诉你"PyPy 在某些领域有巨大潜力",而不是"PyPy 全面碾压 CPython"。
7. 热身为快、冷身为慢:一段代码从"解释"到"机器码"的生命周期
这一节我想带你走一遍 PyPy JIT 的实际执行流程,让那些"听说 PyPy 快"但不清楚具体机制的读者理解,一个循环在 PyPy 内部究竟经历了什么。
7.1 第一遍:解释执行(Cold Run)
PyPy 启动后,首先用它的 RPython 解释器执行 Python 字节码。这个阶段性能并不比 CPython 好多少,甚至因为 PyPy 自身的解释器实现有一定开销,冷启动可能比 CPython 慢一些。
这个阶段 JIT 在默默做一件事:记录执行频率。当某个循环入口的执行次数达到阈值(通过 JITLoop 和函数调用计数判断),它就会被标记为"热循环"。
7.2 第二遍:记录跟踪(Tracing)
一旦循环变热,PyPy 进入跟踪模式。它像一台摄像头一样,记录这个循环首次执行时遇到的所有操作。举例:
python复制for i in range(1000000):
result = process(i)
PyPy 会记录 range 的迭代、i 的类型(int)、函数 process 内部的字节码操作、局部变量的变化轨迹,一直到循环出口。这个跟踪是线性的——只记录实际执行的路径,不分析未走过的分支。
7.3 第三遍:优化与代码生成(Optimization & Codegen)
跟踪得到的中间表示(IR)进入优化流水线。这里做前面提到的各种优化:常量传播、循环不变代码外提、分配提升、逃逸分析、内联。优化完的 IR 再翻译成目标平台(x86-64、ARM64 等)的机器码,并缓存起来。
值得注意的是,这个机器码不仅优化了循环本身,还把循环里调用的函数也内联进来了。因此 PyPy 生成的循环版本往往非常长,但执行路径非常短。
7.4 后续执行:守护与去优化(Guards & Deoptimization)
机器码执行的前提是"跟踪时记录的条件仍然满足"。比如跟踪时发现 i 是 int,机器码里就直接按 int 处理;但万一某次执行时 i 变成了 string,怎么办?
PyPy 的策略是在关键位置插入 guard(守护检查)。每次循环迭代进入前都会检查预设条件,如果条件满足,继续快速执行机器码;如果失败,则去优化(deoptimization),跳回解释器重新执行,再从新路径上积累新的 trace。
这就是为什么 PyPy 特别适合"长期运行、类型稳定"的进程——守护检查失败很少发生,JIT 代码可以持续加速;但如果同一段代码反复横跳类型,JIT 会频繁去优化,性能反而下降。一个典型的例子:同一段循环里一会儿传 int、一会儿传字符串,PyPy 的守护老是被打破,优化价值就大打折扣。
提示:如果有这种"类型不稳定"的热点函数,尽量让同一个循环内保持同构的数据类型,减少守护失败,PyPy 的收益会更大。
7.5 启动开销:为什么 PyPy 不适合短小脚本
理解了生命周期,你就明白 PyPy 的一个特性:它有明显的 JIT warm-up 时间。一个小脚本只跑几秒钟,还没到热点触发阈值就已经结束了,PyPy 的 JIT 根本来不及发挥。因此用 PyPy 跑 hello world 或者简单的文件处理,速度反而可能不如 CPython。
我自己的经验是:能让 PyPy 发挥价值的任务,至少要运行几秒钟以上,并且有明确的循环热点。越长的运行时间、越稳定的类型、越密集的计算,PyPy 的收益越明显。
8. 从 CPython 迁到 PyPy:一份踩过坑的迁移清单
最后这些内容是实打实的经验教训。我迁过好几个项目到 PyPy,踩的坑说多不多,说少不少,挑几个最有代表性的分享出来。
8.1 依赖兼容性:先跑通测试再谈性能
迁移第一步永远是先把测试套件跑绿,而不是先跑 benchmark。 PyPy 对包的支持基本靠 cpyext,大部分纯 Python 包没问题,但 C 扩展包可能有坑。我遇到过的典型问题:
- 某个加密库在新版 PyPy 下编译失败,需要在 PyPy 下重新
pip install,而不是直接把 CPython 的 site-packages 搬过来; cytoolz这类 Cython 加速的库在 PyPy 下需要 fallback 到纯 Python 版本;- 某些包动态加载
.so文件时依赖 CPython 的 ABI,需要检查该包是否有 PyPy 兼容版本。
建议先建一个独立的虚拟环境,用 PyPy 的 pip 安装所有依赖,跑一遍测试。千万别直接复用原来的 venv。
8.2 内存占用和 GC 行为差异
PyPy 进程的内存占用通常比 CPython 高,尤其是在对象创建密集的代码里。我遇到一个数据处理服务,CPython 跑的时候大约占用 1.2GB,PyPy 下涨到 2.5GB,虽然速度快了一倍,但部署机器的内存差点不够用。
好在 PyPy 提供了 --gc 参数和内存相关环境变量,可以调节堆策略。如果你有大规模内存需求,建议先在小数据上测好峰值内存,再决定是否部署。
8.3 嵌入 C/C++ 程序和 C 扩展回调的兼容性
如果你的 Python 代码需要被 C 程序通过 Python C API 嵌入调用,或者你的扩展库大量向 Python 层回调,PyPy 的 cpyext 会更慢,有时甚至有问题。这类场景我不建议用 PyPy,除非你能接受性能损耗。
8.4 部署注意事项:多版本共存与进程管理
PyPy 不是 CPython,不能直接替换系统 Python。部署时我通常这样做:
- 下载 PyPy 官方二进制,解压到单独目录;
- 用
pypy -m venv创建项目虚拟环境; - 在虚拟环境里重新安装依赖;
- 确保启动脚本显式调用 PyPy 解释器路径;
- 用 supervisor 或 systemd 管理进程时,明确指向虚拟环境里的
bin/pypy。
不要试图把 PyPy 当作"系统 python",也不要覆盖系统的 /usr/bin/python,否则包管理器和系统工具可能出问题。
8.5 从开发到生产:一个完整的迁移流程示例
假设你现在有一个纯 Python 服务,想评估 PyPy,可以参考这个流程:
bash复制# 下载 PyPy 并创建独立环境
wget https://downloads.python.org/pypy/pypy3.10-v7.3.15-linux64.tar.bz2
tar -xjf pypy3.10-v7.3.15-linux64.tar.bz2
./pypy3.10-v7.3.15-linux64/bin/pypy -m venv pypy-venv
source pypy-venv/bin/activate
# 安装依赖
pip install -r requirements.txt
# 跑测试
pytest
# 跑一个预热后的基准
python -m timeit "import workload; workload.run()"
跑基准时注意预热:先执行几轮让 JIT 完成编译,再用 timeit 测稳定性能,否则第一轮结果会偏低。
9. 最后分享一点个人体会
写到这里,想跟你分享一个实际心态:PyPy 不是万能钥匙,也不是过时玩具。它是一把很锋利的刀,适合砍"纯 Python 计算密集"这棵树的树枝,但砍不动"C 扩展主导"这种粗树干。
我个人这几年leverage PyPy 最成功的一个案例,是一个纯 Python 的模板渲染引擎。当时团队把它嵌在网关服务里,每个请求都要渲染一份较大的 HTML 模板,里面嵌套了十几层循环和几十个函数调用。CPython 下平均耗时 180 毫秒,PyPy 下压到 25 毫秒,服务端 CPU 占用直接下降 70% 多。整个切换过程只改了解释器版本和虚拟环境,代码一行没动。
如果读到这里你打算试一把,我建议从一个小而真实的热点函数开始实验,而不是一上来就把整个项目切过去。先跑测试、先做基准、先量内存,确认收益再扩大范围。遇到类型不稳定导致的守护失败,试着把数据结构整理得更同质一些;遇到 C 扩展不兼容,考虑改写纯 Python 版本或选择替代库。
PyPy 这条路并不适合所有项目,但它确实值得在每个 Python 性能攻坚的场景里被认真评估一次。哪怕最终不采用,你在评估过程中学到的关于 JIT、对象模型、热点路径的知识,也足够让你对自己写的 Python 代码有更深的体感了。
