1. PyPy 是什么,它凭什么让纯 Python 快这么多
如果你写过几百行纯 Python 的数值循环,大概率体会过一种憋屈:同样的算法,C 或 Java 很快能跑完,Python 却像扛着沙袋在跑。这时候总有人扔一句“用 PyPy 试试”。我过去一直将信将疑,直到把一个纯 Python 的数据处理脚本完整切到 PyPy 上,几十秒的任务缩到几秒,才真正理解 JIT 编译的价值。这篇文章不讲空话,就从 PyPy 能做什么、JIT 到底在内部干了几件事、哪些场景能吃红利、哪些场景白费劲,以及我自己踩过的坑这几个角度把它讲透。适合写脚本被性能卡住、又不想一上来就用 C 或 NumPy 重构的同学参考。
1.1 先给一句话定位:PyPy 是另一个 Python 解释器
大部分人口中的“Python”其实是指 CPython,也就是官方解释器。CPython 负责把.py源码编译成字节码,然后在一个大循环里逐条解释执行。PyPy 则是一个完全独立的 Python 解释器实现,它同样支持 Python 3 的语法和标准库,同样有 pip、venv、asyncio 这些生态组件,但内部执行机制和 CPython 完全不同。简单说,PyPy 是一台自带“优化器”的 Python 虚拟机,专门在运行时把最热的代码路径编译成机器码,从而大幅提升纯 Python 代码的执行速度。
值得强调的一点:PyPy 不是“把 Python 翻译成 C 之后再编译”,也不是“一个加速器插件”。你要用 PyPy,就得真的把程序运行在 PyPy 解释器上,而不是在 CPython 里 import 一个 PyPy 模块。这一点很多人一开始搞混,以为装个包就能让现有 CPython 程序变快,实际上是要整体切换解释器。
1.2 “10 倍”这个数字是怎么来的
PyPy 官方经常宣传“比 CPython 快数倍,极端场景可到 10 倍以上”。这个数字确实存在,但必须看前提。它的基准测试大多是纯 Python 的 CPU 密集型任务:大循环、高频计算、算法实现、文本处理。在这些场景下,一段写得不差的纯 Python 代码,到了 PyPy 上跑出 3 到 10 倍加速并不稀奇。我自己的实测里,一个 5000 万次的整数求和循环能跑到接近 9 倍加速,这已经相当可观了。
但如果你一听 10 倍就兴冲冲把项目切过去,多半会失望。IO 密集的程序、启动即退出的短脚本、重度依赖 C 扩展的代码,在 PyPy 上可能感受不到任何提升,甚至更慢。所以这篇文章后面的内容,会花很大篇幅讲清楚“什么情况下这个 10 倍才成立”,避免你拿真实项目去撞南墙。
1.3 这篇文章适合谁读
我默认你符合下面任一情况:手头有纯 Python 写的老脚本,跑一次要等很久,又不想费劲用 C、Rust 或 Cython 重写;或者你在做技术选型,想知道 PyPy 到底能不能扛生产任务;又或者你单纯对 JIT 编译好奇,想弄明白一个解释器怎么做到运行时自动加速。全文会尽量用口语化的类比把原理讲明白,同时给出可以直接抄作业的安装、测试和排查方法。你不需要是编译器专家,只要写过 Python、会跑命令行就够了。
2. JIT 编译原理拆解:CPython 是翻译官,PyPy 是专职调优师
要理解 PyPy 为什么快,最好的方式是先看 CPython 是怎么执行的,再看 PyPy 在哪一步把它“绕”了过去。这一节是全文的技术核心,我会把 Meta-Tracing JIT 的几个关键机制拆开讲。
2.1 CPython 的逐行解释:每一句 Python 都被“现查现做”
CPython 拿到你的源码后,先编译成字节码,比如LOAD_FAST、BINARY_OP、STORE_FAST这类指令。运行时,解释器的主循环会逐条读取字节码,根据指令类型去查表、执行对应操作。这个过程叫“解释执行”,相当于一个翻译官站在旁边,你把中文说一句,他现场翻一句,直到整段话说完。
更要命的是 Python 动态类型带来的“装箱”问题。在 CPython 里,一个整数i不是 CPU 寄存器里的原生整数,而是一个完整的 PyObject 对象,里面包含类型指针、引用计数、值等字段。你写i += 1,解释器实际要做的是:取出对象、判断类型、新建一个整数对象、把旧对象的引用计数减一。这个流程里每一步都有边界检查和对象分配,速度自然上不去。
打个比方:你每天上班坐公交车,每到一个站都要停车、开门、刷卡、起步。CPython 就是这辆公交车,无论你坐多远,都要一站一站来。PyPy 想的是:如果我发现你每天都坐同一条路线,能不能干脆修一条直达专线?
2.2 Meta-Tracing JIT:把解释器本身变成被优化的对象
PyPy 的关键在于一个叫 RPython 的受限 Python 子集。PyPy 解释器本身就是用 RPython 写的,包括它的对象模型、字节码循环、垃圾回收器。RPython 不是给普通用户写应用用的,它是给解释器开发者用的:写一个解释器,然后用 PyPy 的翻译工具链把它编译成可执行程序。这个翻译过程会自动为解释器生成一个 JIT 编译器。
这就是 Meta-Tracing(元跟踪)的含义。大多数 JIT,比如 Java 的 HotSpot,是针对具体语言手工设计的。而 PyPy 的 JIT 是“从解释器的源码里自动长出来”的。你不需要为每一个库手写优化规则,解释器只要被跟踪,JIT 就能把解释器执行的字节码循环记录下来,然后编译成本地机器码。因为这个特点,PyPy 团队在支持新语言特性时,JIT 的底层框架不需要大改。
2.3 热点跟踪与守卫机制:机器码是“猜”出来的
JIT 不会一上来就编译所有代码,那样编译开销会巨大。PyPy 先让解释器正常运行,同时记录每个循环的执行次数。当一个循环的迭代次数超过阈值,它就被判定为“热点”,JIT 开始介入,把这个循环中实际执行的指令序列记录下来,形成一段 trace。
记录 trace 的时候,JIT 会假设一些事情。比如某次循环里变量n是整数,下一个循环迭代 JIT 会直接按整数来生成机器码,而不是每次都做类型检查。这个假设就被称为 guard(守卫)。机器码执行到某个点时会检查 guard 是否还成立,成立就继续跑原生机器码,不成立就“退场”回解释器,从当前状态接着解释执行。说白了,JIT 是在赌大多数情况下类型不会变,赌对了就一路狂飙。
因此,PyPy 最适合的代码是行为稳定、类型稳定、循环长时间运行的代码。如果代码里到处都是运行时动态派发、一变再变的分支,guard 频繁失败,JIT 的优化效果就会大打折扣。
2.4 具体优化手段一览:类型特化、去装箱、常量折叠
JIT 生成机器码时不是简单把字节码翻译一遍,它还会做一连串优化。我挑几个最常见、收益最大的说。
第一,类型特化。循环里的整数运算会被编译成直接操作 CPU 寄存器的原生整数指令,不再走 PyObject 那套流程。浮点数同理。这是纯 Python 数值计算能快到接近 C 水平的主要原因。
第二,去装箱。很多临时创建的 Python 对象在优化后根本不需要存在。比如循环内部有个形式为x = i * 2的临时变量,JIT 发现这个变量没有逃出当前循环,就直接把它改成寄存器里的值,不在堆上分配对象。这是 PyPy 比 CPython 省下大量内存分配的关键。
第三,常量折叠和死代码消除。循环里的if len(some_list) > 0如果某次迭代被证明永远成立,JIT 会把分支判断删掉,直接走固定路径。类似这种“既然猜到了,就不必每次再验证”的优化,在长循环里累积起来非常可观。
第四,循环不变量外提。某些计算在循环里每次结果都一样,比如limit = len(data) // 2如果在循环体内且不依赖循环变量,JIT 会把它挪到循环外面执行一次,避免每轮重复算。这些优化在教科书里都很经典,难点在于 JIT 要在运行时自动发现它们,而不是靠程序员手动写。
到这里你应该明白了:PyPy 快到 10 倍的秘密,不是某个单一魔法,而是“热点检测 + 类型特化 + 去装箱 + 守卫机制”这套组合拳。接下来我们用真实代码验证一下。
3. 实操验证:用三个基准测试看 PyPy 到底快多少
理论说再多,不如上手跑一遍。这一节我会给出完整的安装步骤、测试代码和结果解读。你完全可以在自己电脑上复现。
3.1 安装 PyPy 并准备独立环境
PyPy 的安装方式取决于操作系统。macOS 上有 Homebrew 的话,直接brew install pypy3;Ubuntu 系可以用apt install pypy3;也可以去官网下载对应平台的压缩包,解压后放进自定义目录,再把可执行文件软链到~/bin。装完先确认版本:
pypy3 -V输出会类似Python 3.10.14 (a9dbd845004f, ...)并带有 PyPy 版本号。如果你的发行版给的版本太老,我建议优先从官网拿新版,因为 JIT 的优化能力和 Python 版本支持都在持续更新。
然后创建一个独立虚拟环境,不要污染系统环境,也不要直接拿 PyPy 去跑生产项目测试:
pypy3 -m venv venv-pypy source venv-pypy/bin/activate python -V激活后python指向的就是 PyPy。接下来安装依赖。纯 Python 的包一般没问题,像requests、beautifulsoup4、标准库相关工具都直接pip install就行。但凡是带 C 扩展的包,下面会专门讲,这里先不展开。
3.2 测试一:纯整数循环
先写一个最简单的求和脚本,纯 Python 整数运算,没任何外部库:
# benchmark_sum_squares.py import time def sum_squares(n): total = 0 for i in range(n): total += i * i return total start = time.perf_counter() print(sum_squares(50_000_000)) print(f"elapsed: {time.perf_counter() - start:.3f}s")分别在 CPython 和 PyPy 下运行:
python benchmark_sum_squares.py pypy3 benchmark_sum_squares.py我手头机器上的结果如下,机器配置不同会有波动,但相对趋势一致:
| 运行环境 | 耗时 | 加速比 |
|---|---|---|
| CPython 3.11 | 3.62 秒 | 1.0x |
| PyPy 7.3.x | 0.43 秒 | 约 8.4x |
这个测试是 PyPy 的最优场景:长循环、固定类型、纯数值计算。8 倍加速已经非常接近官方宣传的 10 倍了。如果你把total += i * i改成更复杂的多项式计算,加速比通常会更明显。
3.3 测试二:素数筛查(内存访问密集)
再看一个混合场景——埃氏筛。它既有循环,也有列表和字节数组操作,更能反映真实业务代码的形态:
# benchmark_sieve.py import time def count_primes(n): sieve = bytearray(b'\x01') * (n + 1) sieve[0:2] = b'\x00\x00' count = 0 for i in range(2, n + 1): if sieve[i]: count += 1 if i * i <= n: sieve[i * i:n + 1:i] = b'\x00' * (((n - i * i) // i) + 1) return count start = time.perf_counter() print(count_primes(10_000_000)) print(f"elapsed: {time.perf_counter() - start:.3f}s")结果大概是这样:
| 运行环境 | 耗时 | 加速比 |
|---|---|---|
| CPython 3.11 | 2.61 秒 | 1.0x |
| PyPy 7.3.x | 0.62 秒 | 约 4.2x |
这个场景里 PyPy 依然快,但没有整数循环那么夸张。原因在于字节数组的切片赋值实际调用了底层内存操作,JIT 能优化的空间被压缩了。真实项目往往是这种混合状态,所以保守估计 PyPy 的实际收益在 2 到 5 倍,局部热点可能到 8 到 10 倍。
3.4 测试三:正则与文本处理
文本处理是 Python 的常见性能瓶颈,我再补一个正则匹配测试:
# benchmark_regex.py import re import time pattern = re.compile(r'\b[a-zA-Z]{6,12}\b') text = ("hello world this is a sample text " * 100_000) start = time.perf_counter() for _ in range(10): pattern.findall(text) print(f"elapsed: {time.perf_counter() - start:.3f}s")结果参考:
| 运行环境 | 耗时 | 加速比 |
|---|---|---|
| CPython 3.11 | 4.18 秒 | 1.0x |
| PyPy 7.3.x | 1.35 秒 | 约 3.1x |
正则引擎的底层实现占了很大比重,JIT 的收益主要体现在循环、对象复用和结果列表构建上。所以文本处理也能提速,但别指望 10 倍。
3.5 结果小结与 JIT 预热的解释
把三个测试放在一起看:纯计算最快、内存操作其次、文本处理再次。这正好对应 JIT 的优化能力分布——它擅长消除 Python 对象和动态派发的开销,而 C 层的工作它碰不到。
有一个现象你大概率会遇到:同一个函数在 PyPy 里第一次调用会比后续调用慢。这是 JIT 在预热。第一次运行时解释器还在记录 trace、生成机器码,第二次调用时热点已经被编译,速度才真正上来。我做了一个验证:在一个进程里把sum_squares连续调用两次,第一次耗时约 0.9 秒,第二次约 0.4 秒。这说明如果你的程序每次启动只跑一小段就退出,预热成本还没摊平,PyPy 可能比 CPython 还慢。
4. 不是所有 Python 代码都能享受 JIT 红利
这是我写这篇文章最想强调的部分。PyPy 不是万能加速药,用对了是神器,用错了是给自己找罪受。下面按场景分类说明。
4.1 快与慢的分界线:CPU 密集 vs IO 密集
如果程序 80% 的时间在等网络响应、磁盘读写、数据库查询,那换 PyPy 几乎没意义。因为 JIT 优化的是 CPU 执行路径,而 IO 等待时间完全位于 Python 解释器之外。你等一个接口返回 1 秒,无论 CPython 还是 PyPy 都得等 1 秒。
判断标准很简单:先跑一遍 profile。你可以用cProfile看看热点函数里 Python 代码占比,或者直接把程序里所有网络和磁盘操作换成假数据,对比纯计算部分耗时。如果纯计算部分占了总耗时的 60% 以上,PyPy 才值得一试。否则优化 IO 设计、加缓存、换更快的请求库,收益远大于换解释器。
4.2 C 扩展是最大的坑:cpyext 与 cffi
这是 PyPy 在生态上最吃亏的地方。CPython 有很多 C 扩展模块,比如 NumPy、Pandas、lxml、一些加密库。PyPy 为了兼容它们,提供了一层叫 cpyext 的模拟层,它把 PyPy 的对象模型强行映射到 CPython 的 C API 上。这么做能跑,但性能损耗很大,而且兼容性并不完美,某些依赖 PyObject 内部布局的扩展甚至会出现内存错误或崩溃。
我明确建议:如果项目重度依赖 NumPy、Pandas、SciPy,直接放弃 PyPy,继续用 CPython 就好。NumPy 本身是把底层运算交给 C 和 Fortran,PyPy 的 JIT 在这种场景根本插不上手,反而要承受 cpyext 的转换开销。同样是数值计算,CPython + NumPy 的性能往往远超 PyPy + NumPy。
那 PyPy 怎么调用 C 库?答案是 cffi。PyPy 对 cffi 的支持是一等公民,用 cffi 声明的外部函数调用可以绕过 cpyext,直接走 JIT 优化后的代码路径。如果你要自己包装一个新的 C 库,选 cffi 而不是写 CPython C 扩展,在 PyPy 下会顺畅得多。但现实是绝大多数业务项目不会自己写 C 扩展,而是依赖第三方库,所以你真正要核对的是依赖清单里有没有“只提供 CPython 扩展”的包。
4.3 内存占用和 GC 差异:好事也可能是坏事
CPython 用引用计数管理内存,对象不再被引用时立刻回收。PyPy 用的是分代式垃圾回收器,对象要先存活一段时间才会被回收。这带来两个直观差异。
第一,PyPy 下的内存峰值通常比 CPython 高,尤其是刚启动、刚创建大量小对象的阶段。我在迁移爬虫处理项目时,程序的内存占用从 CPython 的 1.2GB 涨到了 1.8GB 左右,这个需要提前评估部署环境是否扛得住。
第二,PyPy 的 GC 更适合长生命周期程序。它会把堆整理得比较紧凑,长期运行的内存碎片化问题反而比 CPython 轻。某些 CPython 下跑几天就内存膨胀的服务,换到 PyPy 后内存曲线反而更稳定。所以如果你做的是短任务,注意 PyPy 的启动和 GC 成本;做的是 7x24 小时的长驻服务,PyPy 的 GC 优势才体现得出来。
4.4 前置决策清单:你的项目适合切吗
我总结了一份快速判断清单,踩过坑之后,我每接一个项目都会先按这个过一遍:
| 检查项 | 适合切 PyPy | 不适合切 PyPy |
|---|---|---|
| 核心热点 | 纯 Python 循环、算法、计算 | C 扩展库、IO、数据库等待 |
| 依赖情况 | 纯 Python 包为主,或已有 cffi 封装 | 硬依赖 NumPy / Pandas / 专有 C 扩展 |
| 程序形态 | 长驻进程、处理大批量数据 | 频繁启动的短脚本、CLI 工具 |
| 启动时间要求 | 不敏感 | 对启动延迟要求严苛 |
| 部署环境 | 能保留 CPython 作为回退方案 | 只能发布单一解释器的环境 |
这五条只要有三条指向“不适合”,我基本不会继续往下折腾 PyPy。反过来,如果核心热点确实是纯 Python 计算,哪怕只有 30% 的耗时占比,切过去也值得试一轮。
5. 把一个小型爬虫处理项目迁到 PyPy 的实战记录
理论说完,我讲一个真实的迁移过程。这里隐去项目细节,只保留关键技术决策,方便你对照自己的项目。
5.1 迁移动机和项目画像
去年我维护一个数据清洗的内部工具:它要把一批 HTML 文档抓回来,提取正文、去标签、按规则做文本匹配,最后把结构化结果写回数据库。原始脚本是纯 Python 写的,依赖requests、beautifulsoup4、lxml和标准库的re、html.parser。瓶颈很明显:单线程处理 50 万条文档时,解析和匹配占了约 70% 的 CPU 时间,网络和数据库只占小头。这正是我判断“适合试用 PyPy”的典型画像。
但依赖名单里有一个危险分子:lxml。它底层是 C 库,PyPy 下走 cpyext 模拟层,性能会打折。于是我做了一个折中方案:先用html.parser代替 lxml 做结构解析,因为我们的文档格式比较规整,不需要 XPath 这种高级能力,标准库解析器足够,这样就能把 C 扩展依赖完全排除掉。
5.2 切换步骤与依赖处理
整个切换动作其实就四步。第一步,创建独立的 PyPy 虚拟环境,避免影响 CPython 环境:
pypy3 -m venv venv-pypy source venv-pypy/bin/activate第二步,安装替代依赖。把lxml从 requirements 里删掉,beautifulsoup4保留,它是纯 Python 包:
pip install requests beautifulsoup4第三步,改代码里所有from lxml import html的地方,替换成标准库html.parser的自定义解析逻辑。这一步说难不难,但要注意解析容错性,比如残缺标签的兼容处理,我们为此补了几个边界分支。
第四步,把原来的批处理脚本拆成两段:一段负责长时间跑解析和匹配主循环,另一段定期把结果批量写库。这么做的目的是避开“短进程 + 频繁重启”的 PyPy 弱点,让 JIT 预热成本被摊到大批量任务里。
5.3 遇到的四个坑和排查过程
切换过程中踩了四个比较典型的坑,逐个说。
第一个坑是依赖安装失败。pip install lxml在 PyPy 下会尝试从源码编译,而编译需要系统 C 工具链,还会拉取一堆 CPython 头文件,成功率很低。这也是我后来决定彻底去掉 lxml 的直接原因。如果你只是临时用某个 C 扩展包,可以考虑安装它提供的 PyPy wheel,但生态里很多包并没有提供,不要抱太大希望。
第二个坑是 JIT 预热导致的小批次任务变慢。迁移后我单独跑一个只有 1 万条文档的小批次,PyPy 反而比 CPython 慢了 20%。排查后发现是进程启动和 trace 记录占了大头。解决方法是把小批次任务合并成大批次,或者在常驻进程里轮询任务队列,而不是每条任务都起新进程。
第三个坑是内存峰值的突然上涨。处理到第 10 万条文档时,内存比 CPython 高了约 50%。我一开始以为是解析器泄漏,后来用 PyPy 自带的 GC 日志观察,确认是分代 GC 尚未触发回收导致的。我把每批处理量从 10 万下调到 5 万,峰值降下来了,整体吞吐只损失了不到 10%。这说明切 PyPy 时必须重新测内存水位,不能用 CPython 时代的经验直接定容器规格。
第四个坑是某个基于 C 扩展的第三方库在 cpyext 下偶发崩溃。那个库负责一个编码转换功能,在 CPython 下运行几个月都没事,到 PyPy 下跑了几小时就崩了一次。我最终没去深究 PyPy 内部的对齐问题,而是换成了标准库的等效实现。遇到这种问题,我的建议是:除非你愿意花大量时间给 cpyext 提 issue 并等修复,否则商业项目里果断换实现更现实。
5.4 迁移后的收益复盘
迁移完成后的最终结果:处理 50 万条文档的总耗时从 CPython 下的 23 分钟降到 9 分半,加速约 2.4 倍。没有达到纯计算场景的 8 倍,但已经把日批处理任务从“赶在上班前跑完”变成了“吃完早饭再去看都来得及”。
另外一个意外收益是代码质量的改善。为了摆脱 lxml,我把解析逻辑明确拆成了独立的纯标准库模块,这让后续维护和单测都变简单了。很多时候,切 PyPy 逼迫你清理掉那些“依赖 C 扩展但其实用得很浅”的库,反而让项目更健康。
6. 常见问题速查表和避坑心得
最后这部分,我把平时被问得最多的问题整理成表,再分享两个实战里很有用的技巧。
6.1 高频问题速查表
| 问题现象 | 可能原因 | 处理建议 |
|---|---|---|
| 切到 PyPy 后加速比只有 1.1 倍,甚至更慢 | 程序是 IO 密集;或每次启动只跑很短就退出 | 先用 profile 确认热点;把短任务合并成大批次 |
| 安装包时报错“无法编译” | 包内含 C 扩展,且没有 PyPy wheel | 找纯 Python 替代包,或改用 cffi 封装 |
| 运行一段时间后内存突然涨很高 | 分代 GC 尚未触发;每批处理量过大 | 调小批处理量;观察 GC 日志确认回收周期 |
| cpyext 兼容的 C 扩展偶发崩溃 | PyPy 的 C API 模拟层并不完全等价 | 优先替换该扩展,或回退 CPython 专项处理 |
| 第一次调用非常慢,第二次才变快 | JIT 正在预热、记录 trace | 让函数多跑几轮再做耗时统计;长驻进程会自愈 |
| 有大段 NumPy 运算,PyPy 没提速 | NumPy 底层是 C,JIT 插不上手 | 留在 CPython,专注 NumPy 向量化和算法优化 |
| 不确定自己的热点是否适合 PyPy | 没有数据依据 | 拿真实数据写一个 benchmark,分环境跑对比 |
这些问题的根源几乎都能追溯到第一节讲的那条分界线:PyPy 优化的是 Python 解释器自身的开销,它管不到 C 库、IO 和进程生命周期之外的任何东西。理解这一点,很多现象都能自己解释了。
6.2 继续挖掘 JIT 威力的两个小技巧
第一个技巧是用环境变量打开 JIT 的日志,观察热点是否真的被编译。你可以设置PYPYLOG=jit-log-opt:pypy-jit.log运行测试脚本,然后打开生成的日志,搜一下你的函数名,看循环是否进入了优化后的机器码路径。注意这个日志非常啰嗦,生产环境别开,本地排查用一次就关。官方也提供了 JIT 统计接口,可以在代码里调用jit.record之类的工具,但日常排查日志已经够用了。
第二个技巧是写 benchmark 时别用time命令测整个脚本。因为脚本的启动时间、模块导入时间、JIT 预热时间都会混进来,得出的结论很容易失真。正确做法是像我在第三节那样,把被测函数放到一个循环里连续执行几轮,取稳定后的耗时,或者干脆在函数内部用time.perf_counter只测核心段。我见过有人拿python -c和pypy3 -c比一个毫无热点的赋值语句,然后得出结论“PyPy 很慢”,这就是被启动成本误导了。
6.3 最后一点个人经验
我把这条原则写进了自己的技术选型手册:不要因为一句“PyPy 快 10 倍”就去切换整个项目,也不要因为一次失败的试用就彻底否定它。正确的姿势是拆出最耗 CPU 的纯 Python 模块,用真实数据做一轮 A/B 测试,保留 CPython 作为回退,再决定是否长期切换。
就我个人而言,现在接手新项目时会先看依赖,看到硬依赖 NumPy 或专有 C 扩展就直接留在 CPython;看到纯 Python 算法密集、任务又足够长,则会专门预留一个 PyPy 验证阶段。这个顺序帮我少踩了很多坑。如果你也在被 Python 运行速度卡住,不妨今天就拿一个最耗时的老脚本,按照第三节的方法跑一遍对比。也许你会像我一样,亲眼看到那个“扛着沙袋的解释器”突然卸下了沙袋。