tasklab 的 cpu_work 在 CPython 上每次约要十几毫秒;把同一个文件交给 PyPy,五次调用里的第一次数量级也可能与后四次不同。仅凭这个差异不能说“换解释器就能加速任务应用”:先要确定项目能否导入、四个操作的结果是否相同,再谈被计时的究竟是哪段代码。

本篇沿用 39 的可安装 wheel 和 36 的测量边界。当前 completion 基线是 CPython 3.12.3、free-threaded CPython 3.13.9,以及 macOS arm64 上的 PyPy 8.0.0(Python 3.12.14,官方归档)。解释器版本、项目要求的 Python 版本和 wheel 的安装位置分别记录;本次没有用 PyPy 3.9 代跑当前项目,因为 requires-python = ">=3.12" 已把它排除在验收范围之外。

先看能否完成合同

PyPy 8.0.0 的当前环境记录在 examples/python-language/evidence/completion/acceptance/pypy-environment.stdout:Python 是 3.12.14,实现是 PyPy,可执行文件来自 /private/tmp/python-language-completion/pypy/pypy3.12-v8.0.0-macos_arm64/bin/pypy3.12,该可执行文件 SHA-256 为 bc17ab8bf1f3ebdd4d46e90225afb08bb3a67a66fd5eda1507fc52effee3d0db。从安装环境运行时必须确认模块路径指向对应 venv 的 site-packages,不能让源码树冒充 wheel。只绕过包导入、运行一个函数,不算应用兼容。

初轮安装后回归有 2 项失败、2 项跳过,原始 acceptance/pypy-tests.stderr 保留。内存实验直接导入 PyPy 未提供的 _tracemalloc,字节码测试则硬编码了 CPython 的 BINARY_OP。修复后同一 PyPy 8.0.0 环境运行 105 项测试,103 通过、2 项原有跳过,没有新增跳过项;CPython 3.12.3 为 105 项通过,free-threaded 3.13.9 为 104 通过、1 项跳过。新记录在 writing-plans/python-language/completion/evidence/pypy-repair/。内存脚本现在按实现选择接口:CPython 使用 tracemalloc;PyPy 使用官方 gc.get_stats(memory_pressure=True),记录处理前、处理中和 GC 后的 GC used/allocated、历史峰值、JIT 内存及外部分配压力。后者是堆聚合快照,不声称提供逐分配调用栈。字节码测试按实现分别要求 BINARY_OP 和 BINARY_ADD,业务结果断言保留。PyPy 的两项原有跳过仍分别是 CPython C API 专属实验和不可用的私有子解释器。CPython 38 的执行循环和 GIL 不能直接套在 PyPy 上。

暖机和计时范围

experiments/E04_compare.py 用 runpy 读取同一份 src/tasklab/workloads.py,不是为另一解释器重写校验算法。输入是 "one two one " * 50(600 字符),iterations=120,四组分别在额外调用 0、5、20 次后及连续暖机至少 3 秒后记录 5 次 perf_counter_ns();各组都核对 checksum=754902074。这里计的是单次完整 cpu_work 调用,不包含启动、导入、SQLite、队列、网络和词频任务响应。当前 PyPy 8.0.0 的 0 次暖机样本是 2,603,291、761,500、244,875、231,833、244,459 ns;至少 3 秒暖机后的五个样本范围是 227,916–239,583 ns。相同脚本下,CPython 3.12.3 的 3 秒暖机样本范围是 2,564,042–2,873,084 ns,free-threaded CPython 3.13.9 的范围是 2,855,166–2,926,000 ns。每个数都只对这段纯 Python CPU 函数、这台机器、这几个解释器构建有效。PyPy 官方 FAQ 也提醒 JIT 有暖机期;此处不据此推断整个服务的吞吐或所有 PyPy 版本的速度。

复验与迁移边界

1
2
3
PYTHON=/private/tmp/python-language-completion/installed-pypy/bin/python \
examples/python-language/experiments/E04_compare.py \
--workloads examples/python-language/src/tasklab/workloads.py

命令必须和安装环境、当前 wheel、模块路径一起保存;只贴一段耗时输出不足以证明应用兼容。examples/python-language/evidence/completion/acceptance/pypy-environment.stdout、pypy-tests.stderr 及 examples/python-language/evidence/completion/additional/*warmed-compare.stdout 保留初轮环境、样本和失败堆栈;修复后的测试命令和结果见 writing-plans/python-language/completion/evidence/pypy-repair/after.json。缺失 PyPy 的机器不能用 CPython 代跑。选修 C 扩展编译产物带 CPython ABI 标记,没有在 PyPy 上加载,跨实现扩展兼容为 NOT_RUN。不应据此宣称缓冲区扩展可移植。

预测题。 只运行 PyPy 8.0.0 上的 E04_compare.py,输出五个正确校验值后,能否把该解释器记成 tasklab 全套兼容?答案:不能。计时代码只读取 workloads.py 的函数;还要看已安装 wheel 的导入路径、提交、查询、取消、SQLite 以及全套测试。当前回归没有失败,但 PyPy GC 聚合快照不等于 CPython 逐分配追踪,CPython C API 专属实验和私有子解释器仍有能力边界。

变更题。 把 BINARY_OP 的硬编码断言改为同时接受两种指令名,是否足以让 PyPy 全套测试通过?答案:不能。内存入口还要明确处理 _tracemalloc 缺失并使用 PyPy 原生 GC 快照;字节码应按当前实现检查,不混合两种实现的要求。修复后必须复跑业务、资源与内存实验,并保留两类内存接口口径和原有跳过项的边界。

参考资料

  • PyPy 8.0.0 发布说明、下载及校验和、常见问题(JIT 与性能):支持版本、beta 状态及暖机限制。
  • Python itertools.batched:3.12 的 API;不因旧解释器能运行个别函数便推断整个项目可导入。
  • examples/python-language/evidence/completion/acceptance/pypy-environment.stdout、pypy-tests.stderr 与 examples/python-language/evidence/completion/additional/*warmed-compare.stdout:安装来源、命令、样本、测试与失败原文。