PEP 3146 – 将 Unladen Swallow 合并到 CPython
- 作者:
- Collin Winter <collinwinter at google.com>, Jeffrey Yasskin <jyasskin at google.com>, Reid Kleckner <rnk at mit.edu>
- 状态:
- 已撤回
- 类型:
- 标准跟踪
- 创建日期:
- 2010年1月1日
- Python 版本:
- 3.3
- 发布历史:
PEP 撤回
摘要
本 PEP 提议将 Unladen Swallow 项目 [3] 合并到 CPython 的源码树中。Unladen Swallow 是一个专注于性能的 CPython 开源分支。Unladen Swallow 与有效的 Python 2.6.4 应用程序和 C 扩展模块在源代码上兼容。
Unladen Swallow 为 CPython 添加了一个即时(JIT)编译器,允许将选定的 Python 代码编译为优化后的机器码。除了传统的静态编译器优化外,Unladen Swallow 的 JIT 编译器还可以利用运行时收集的数据来做出关于代码行为的已验证假设,从而生成更快的机器码。
本 PEP 提议将 Unladen Swallow 集成到 CPython 的开发树中,放在一个单独的 py3k-jit
分支,最终目标是与主 py3k
分支合并。虽然 Unladen Swallow 远未完成或完美,但我们认为 Unladen Swallow 已达到足够的成熟度,足以纳入 CPython 的路线图。我们致力于创建一个稳定的平台,供更广泛的 CPython 开发团队在此基础上构建,一个能持续提升性能的平台。
本 PEP 将详细介绍 Unladen Swallow 的实现及其与 CPython 2.6.4 的区别;用于衡量性能的基准测试;用于确保正确性和兼容性的工具;对 CPython 当前平台支持的影响;以及对 CPython 核心开发流程的影响。最后,PEP 将提出一个合并计划,并简要说明未来工作的可能方向。
我们寻求BDFL的以下支持:
- 批准按照下述设计为 CPython 添加即时编译器的总体概念。
- 允许我们在 CPython 源代码树中继续从事即时编译器的开发。
- 在解决所有阻碍性问题 [31] 后,允许最终将即时编译器合并到
py3k
分支。 - 一匹小马。
基本原理,实现
许多公司和个人希望 Python 运行得更快,以便将其用于更多项目。Google 就是其中一家公司。
Unladen Swallow 是一个由 Google 支持的 CPython 分支,旨在提高 Google 大量 Python 库、工具和应用程序的性能。为最大程度地方便 Unladen Swallow 的采用,该项目最初设定了四个目标:
- 与 CPython 2.6.4 基线相比,单线程代码性能提升 5 倍。
- 与有效的 CPython 2.6 应用程序 100% 源代码兼容。
- 与有效的 CPython 2.6 C 扩展模块 100% 源代码兼容。
- 设计为最终能够合并回 CPython。
我们选择 2.6.4 作为基线是因为 Google 内部使用 CPython 2.4,直接从 CPython 2.4 跳到 CPython 3.x 被认为不可行。
为实现期望的性能,Unladen Swallow 采用了即时(JIT)编译器 [51],继承了 Urs Hoelzle 在 Self 上的工作 [52],在运行时收集反馈数据并用其指导编译时优化。这与当前 JavaScript 引擎 [59]、[60];大多数 Java 虚拟机 [63];Rubinius [61]、MacRuby [62] 及其他 Ruby 实现;Psyco [64] 等所采取的方法类似。
我们明确拒绝任何关于我们的想法是原创的说法。我们力求在可能的情况下重用其他研究人员已发表的工作。如果存在任何原创工作,那也纯属偶然。我们尽可能尝试吸取学术界和工业界的各种优秀想法。一份指导 Unladen Swallow 的研究论文列表可在 Unladen Swallow wiki [54] 上找到。
关于优化动态语言的一个关键观察是,它们理论上是动态的;实际上,每个函数或代码片段相对而言是静态的,使用一组稳定的类型和子函数。当前 CPython 字节码解释器对它运行的代码持最坏的假设,即用户随时可能覆盖 `len()` 函数或将从未见过的类型传递给函数。实际上这种情况从未发生,但用户代码却为此付费。Unladen Swallow 利用了用户代码相对静态的特性来提高性能。
从高层次来看,Unladen Swallow JIT 编译器通过将 CPython 字节码翻译成特定平台的机器码来实现,它利用运行时收集的数据以及经典的编译器优化来提高生成机器码的质量。因为我们只想花费资源来编译真正能提高程序运行时的 Python 代码,所以我们使用在线启发式算法来评估给定函数的“热度”。一旦函数的“热度”值超过给定阈值,它就会被选中进行编译和优化。然而,直到函数被认为是“热”的,它都将在标准的 CPython eval 循环中运行,Unladen Swallow 对此循环进行了仪器化,以记录执行的每个字节码的有趣数据。这些运行时数据被用来减少生成机器码的灵活性,使我们能够优化常见情况。例如,我们收集的数据包括:
- 一个分支是否被采取/未被采取。如果一个分支从未被采取,我们将不将其编译为机器码。
- 运算符使用的类型。如果我们发现 `a + b` 总是对整数进行加法运算,那么该代码片段生成的机器码将不支持浮点数加法。
- 每个调用点使用的函数。如果我们发现一个特定的 `foo()` 调用点总是调用同一个 `foo` 函数,我们可以优化该调用或将其内联。
有关收集到的数据点及其用法,请参阅 [55]。
然而,如果碰巧历史上一度未被采取的分支现在被采取了,或者某个为整数优化的 `a + b` 代码片段接收了两个字符串,我们就必须支持这种情况。我们不能改变 Python 的语义。这些优化后的机器码段中的每一个前面都有一个 `guard`,它会检查我们在优化时所做的简化假设是否仍然成立。如果假设仍然有效,我们就执行优化后的机器码;否则,我们就回退到解释器并继续执行。
我们选择重用一个名为 LLVM 的现有编译器库集 [4] 来进行代码生成和代码优化。这使我们的小团队免于理解和调试多台机器指令集的代码生成,也免于实现一套大量的经典编译器优化。没有这样的代码重用,本项目是不可能实现的。我们发现 LLVM 易于修改,并且其社区对我们的建议和修改持开放态度。
更详细地说,Unladen Swallow 的 JIT 通过将 CPython 字节码编译到 LLVM 自己的中间表示(IR)[94] 来工作,同时考虑了 CPython eval 循环的任何运行时数据。然后,我们运行一套 LLVM 内置的优化过程,生成一个更小、更优化的原始 LLVM IR 版本。LLVM 然后将 IR 降低到特定平台的机器码,执行寄存器分配、指令调度以及任何必要的重定位。这种编译管道的安排允许通过传递 `--without-llvm` 给 `./configure` 来轻松地从编译后的 `python` 二进制文件中省略基于 LLVM 的 JIT;后面会讨论此标志的各种使用场景。
有关 Unladen Swallow 工作原理的完整细节,请参阅 Unladen Swallow 文档 [53]、[55]。
Unladen Swallow 专注于提高单线程、纯 Python 代码的性能。我们没有努力移除 CPython 的全局解释器锁(GIL);我们认为这与我们的工作是分开的,并且由于其敏感性,最好在主开发分支中进行。我们考虑过将 GIL 移除作为 Unladen Swallow 的一部分,但担心在将我们的工作从 CPython 2.6 移植到 3.x 时引入细微的错误。
JIT 编译器是一个极其通用的工具,我们远未穷尽其全部潜力。我们试图创建一个足够灵活的框架,以便更广泛的 CPython 开发社区能够在未来几年内在此基础上构建,并在后续的每个版本中提取更高的性能。
备选方案
我们考虑过许多提高 Python 性能的替代策略,但发现它们都不令人满意。
- Cython, Shedskin:Cython [101] 和 Shedskin [102] 都是 Python 的静态编译器。我们认为它们是 CPython 历史性能不佳的有用但有限的解决方法。Shedskin 不支持完整的 Python 标准库 [103],而 Cython 需要手动进行 Cython 特定的注释才能获得最佳性能。
像这样的静态编译器在编写扩展模块时很有用,无需担心引用计数,但由于它们是静态的、提前(ahead-of-time)的编译器,它们无法优化由了解运行时数据的即时编译器所考虑的全部代码范围。
- IronPython:IronPython [106] 是微软 .Net 平台上的 Python。它在 Mono [107] 上并未得到积极测试,这意味着它基本上只适用于 Windows,不适合作为通用的 CPython 替代品。
- Jython:Jython [108] 是 Python 2.5 的完整实现,但比 Unladen Swallow 慢得多(在测量的基准测试中慢 3-5 倍),并且不支持 CPython 扩展模块 [109],这将使大型应用程序的迁移成本过高。
- Psyco:Psyco [64] 是 CPython 的一个专门化的 JIT 编译器,实现为一个扩展模块。它主要提高了数值代码的性能。优点:存在;使某些代码更快。缺点:仅支持 32 位,没有 64 位支持计划;仅支持 x86;维护难度极大;由于对齐问题,与 SSE2 优化代码不兼容。
- PyPy:PyPy [65] 在数值代码上表现良好,但在某些工作负载上比 Unladen Swallow 慢。从 CPython 迁移到 PyPy 的大型应用程序成本过高:PyPy 的 JIT 编译器仅支持 32 位 x86 代码生成;重要的模块,如 MySQLdb 和 pycrypto,无法针对 PyPy 构建;PyPy 不提供嵌入式 API,更不用说与 CPython 相同的 API。
- PyV8:PyV8 [110] 是一个 alpha 阶段的实验性 Python 到 JavaScript 编译器,运行在 V8 之上。PyV8 不实现整个 Python 语言,也不支持 CPython 扩展模块。
- WPython:WPython [104] 是 CPython 解释器循环的一个基于字码(wordcode)的重新实现。虽然它提供了对解释器性能的适度改进 [105],但它不是即时编译器的非此即彼的替代品。解释器永远无法像优化后的机器码那样快。我们认为 WPython 和类似的解释器增强是对我们工作的补充,而不是竞争对手。
性能
基准测试
Unladen Swallow 开发了一套相当大的基准测试套件,范围从旨在测试单个功能的合成微基准测试到整个应用程序的宏基准测试。这些基准测试的灵感分别来自第三方贡献者(如 `html5lib` 基准测试)、Google 自己的内部工作负载(`slowspitfire`、`pickle`、`unpickle`),以及在更广泛的 Python 社区中被大量使用的工具和库(`django`、`2to3`、`spambayes`)。这些基准测试通过一个名为 `perf.py` 的单一接口运行,该接口负责收集内存使用信息、绘制性能图表,并对基准测试结果进行统计分析以确保其显著性。
完整的可用基准测试列表可在 Unladen Swallow wiki [43] 上找到,包括下载和自行运行基准测试的说明。我们所有的基准测试都是开源的;没有 Google 专有的。我们相信这个基准测试集合是衡量任何完整 Python 实现的有用工具,事实上,PyPy 已经在使用这些基准测试进行自己的性能测试 [80]、[95]。我们对此表示欢迎,并希望从 Python 社区那里为基准测试套件获得额外的负载。
我们集中精力收集宏基准测试以及尽可能模拟真实应用程序的基准测试,当无法运行整个应用程序时。从另一个角度看,我们的基准测试集合最初侧重于 Google Python 代码看到的工作负载(Web 应用程序、文本处理),尽管我们后来已将其扩展到包括 Google 不关心的工作负载。到目前为止,我们回避了大量数值工作负载,因为 NumPy [79] 在此类代码上已经做得很好,因此提高数值性能不是团队的初始高优先级;我们已经开始将此类基准测试纳入集合 [96],并已开始着手优化数值 Python 代码。
除了这些基准测试,还有一些我们明确不感兴趣的基准测试。Unladen Swallow 专注于提高纯 Python 代码的性能,因此 NumPy 等扩展模块的性能并不重要,因为 NumPy 的核心例程是用 C 实现的。同样,涉及大量 IO 的工作负载,如 GUI、数据库或高 IO 的应用程序,我们认为将无法准确衡量解释器或代码生成优化。尽管如此,标准库中的 C 语言扩展模块的性能肯定有改进空间,因此,我们已添加了 `cPickle` 和 `re` 模块的基准测试。
与 CPython 2.6.4 的性能对比
下表比较了 CPython 2.6.4 和 Unladen Swallow 多次基准测试迭代的算术平均值。`perf.py` 收集的数据比这要多,而且算术平均值并非全部;为简洁起见,我们仅重现平均值。为表示结果的显著性,我们包含了 95% 置信区间的 Student 双尾 t 检验 [44] 的 `t` 值。大多数基准测试运行 100 次迭代,但一些运行时间较长的整个应用程序基准测试迭代次数较少。
每个基准测试的描述可在 Unladen Swallow wiki [43] 上找到。
命令
./perf.py -r -b default,apps ../a/python ../b/python
32 位;gcc 4.0.3;Ubuntu Dapper;Intel Core2 Duo 6600 @ 2.4GHz;2 核;4MB L2 缓存;4GB RAM
基准测试 | CPython 2.6.4 | Unladen Swallow r988 | 变化 | 显著性 | 时间线 |
---|---|---|---|---|---|
2to3 | 25.13 秒 | 24.87 秒 | 快 1.01 倍 | t=8.94 | http://tinyurl.com/yamhrpg |
django | 1.08 秒 | 0.80 秒 | 快 1.35 倍 | t=315.59 | http://tinyurl.com/y9mrn8s |
html5lib | 14.29 秒 | 13.20 秒 | 快 1.08 倍 | t=2.17 | http://tinyurl.com/y8tyslu |
nbody | 0.51 秒 | 0.28 秒 | 快 1.84 倍 | t=78.007 | http://tinyurl.com/y989qhg |
rietveld | 0.75 秒 | 0.55 秒 | 快 1.37 倍 | 不显著 | http://tinyurl.com/ye7mqd3 |
slowpickle | 0.75 秒 | 0.55 秒 | 快 1.37 倍 | t=20.78 | http://tinyurl.com/ybrsfnd |
slowspitfire | 0.83 秒 | 0.61 秒 | 快 1.36 倍 | t=2124.66 | http://tinyurl.com/yfknhaw |
slowunpickle | 0.33 秒 | 0.26 秒 | 快 1.26 倍 | t=15.12 | http://tinyurl.com/yzlakoo |
spambayes | 0.31 秒 | 0.34 秒 | 慢 1.10 倍 | 不显著 | http://tinyurl.com/yem62ub |
64 位;gcc 4.2.4;Ubuntu Hardy;AMD Opteron 8214 HE @ 2.2 GHz;4 核;1MB L2 缓存;8GB RAM
基准测试 | CPython 2.6.4 | Unladen Swallow r988 | 变化 | 显著性 | 时间线 |
---|---|---|---|---|---|
2to3 | 31.98 秒 | 30.41 秒 | 快 1.05 倍 | t=8.35 | http://tinyurl.com/ybcrl3b |
django | 1.22 秒 | 0.94 秒 | 快 1.30 倍 | t=106.68 | http://tinyurl.com/ybwqll6 |
html5lib | 18.97 秒 | 17.79 秒 | 快 1.06 倍 | t=2.78 | http://tinyurl.com/yzlyqvk |
nbody | 0.77 秒 | 0.27 秒 | 快 2.86 倍 | t=133.49 | http://tinyurl.com/yeyqhbg |
rietveld | 0.74 秒 | 0.80 秒 | 慢 1.08 倍 | t=-2.45 | http://tinyurl.com/yzjc6ff |
slowpickle | 0.91 秒 | 0.62 秒 | 快 1.48 倍 | t=28.04 | http://tinyurl.com/yf7en6k |
slowspitfire | 1.01 秒 | 0.72 秒 | 快 1.40 倍 | t=98.70 | http://tinyurl.com/yc8pe2o |
slowunpickle | 0.51 秒 | 0.34 秒 | 快 1.51 倍 | t=32.65 | http://tinyurl.com/yjufu4j |
spambayes | 0.43 秒 | 0.45 秒 | 慢 1.06 倍 | 不显著 | http://tinyurl.com/yztbjfp |
许多基准测试在 Unladen Swallow 下性能有所下降,因为当前版本会阻塞执行以将 Python 函数编译为机器码。这导致了 `html5lib` 和 `rietveld` 基准测试的时间线图表中出现的行为,例如,并减慢了 `2to3` 的整体性能。我们有一个活跃的开发分支来解决这个问题([46]、[47]),但要在 CPython 当前的线程系统的限制下工作,这个过程比最初预期的要复杂得多,需要更多的关注和时间。我们认为这个问题对于最终合并到 `py3k` 分支至关重要。
我们显然没有达到最初的 5 倍性能提升的目标。紧随其后的是性能回顾 性能回顾,它解释了我们为何未能达到最初的性能目标。我们维护了一份尚未实现的性能工作列表 [50]。
内存使用
下表显示了 CPython 2.6.4 和 Unladen Swallow r988 在 Unladen Swallow 的默认基准测试中的最大内存使用量(以千字节为单位),以及基准测试生命周期内的内存使用量时间线。我们包括了 32 位和 64 位二进制文件的表格。内存使用量在 Linux 2.6 系统上通过对内核的 `/proc/$pid/smaps` 伪文件中的 `Private_` 部分进行求和来测量 [45]。
命令
./perf.py -r --track_memory -b default,apps ../a/python ../b/python
32 位
基准测试 | CPython 2.6.4 | Unladen Swallow r988 | 变化 | 时间线 |
---|---|---|---|---|
2to3 | 26396 KB | 46896 KB | 1.77 倍 | http://tinyurl.com/yhr2h4z |
django | 10028 KB | 27740 KB | 2.76 倍 | http://tinyurl.com/yhan8vs |
html5lib | 150028 KB | 173924 KB | 1.15 倍 | http://tinyurl.com/ybt44en |
nbody | 3020 KB | 16036 KB | 5.31 倍 | http://tinyurl.com/ya8hltw |
rietveld | 15008 KB | 46400 KB | 3.09 倍 | http://tinyurl.com/yhd5dra |
slowpickle | 4608 KB | 16656 KB | 3.61 倍 | http://tinyurl.com/ybukyvo |
slowspitfire | 85776 KB | 97620 KB | 1.13 倍 | http://tinyurl.com/y9vj35z |
slowunpickle | 3448 KB | 13744 KB | 3.98 倍 | http://tinyurl.com/yexh4d5 |
spambayes | 7352 KB | 46480 KB | 6.32 倍 | http://tinyurl.com/yem62ub |
64 位
基准测试 | CPython 2.6.4 | Unladen Swallow r988 | 变化 | 时间线 |
---|---|---|---|---|
2to3 | 51596 KB | 82340 KB | 1.59 倍 | http://tinyurl.com/yljg6rs |
django | 16020 KB | 38908 KB | 2.43 倍 | http://tinyurl.com/ylqsebh |
html5lib | 259232 KB | 324968 KB | 1.25 倍 | http://tinyurl.com/yha6oee |
nbody | 4296 KB | 23012 KB | 5.35 倍 | http://tinyurl.com/yztozza |
rietveld | 24140 KB | 73960 KB | 3.06 倍 | http://tinyurl.com/ybg2nq7 |
slowpickle | 4928 KB | 23300 KB | 4.73 倍 | http://tinyurl.com/yk5tpbr |
slowspitfire | 133276 KB | 148676 KB | 1.11 倍 | http://tinyurl.com/y8bz2xe |
slowunpickle | 4896 KB | 16948 KB | 3.46 倍 | http://tinyurl.com/ygywwoc |
spambayes | 10728 KB | 84992 KB | 7.92 倍 | http://tinyurl.com/yhjban5 |
内存使用量增加的原因包括:a) LLVM 代码生成、分析和优化库;b) 本地代码;c) LLVM 中的内存使用问题或泄漏;d) 优化和生成机器码所需的数据结构;e) 尚未分类的其他来源。
虽然自最初的朴素 JIT 实现以来,我们在减少内存使用量方面取得了重大进展 [42],但显然还有更多工作要做。我们相信在不牺牲性能的情况下仍有进一步节省内存的空间。我们倾向于专注于原始性能,尚未在降低内存使用量方面进行协同努力。我们将降低内存使用量视为最终合并到 `py3k` 分支的阻碍性问题。我们寻求社区在可接受的内存使用量增加水平上的指导。
启动时间
静态链接 LLVM 的代码生成、分析和优化库会增加 Python 二进制文件启动所需的时间。LLVM 使用的 C++ 静态初始化器也会增加启动时间,此外还包括导入我们希望内联到 Python 代码中的预编译 C 运行时例程的集合。
Unladen Swallow 的 `startup` 基准测试结果
$ ./perf.py -r -b startup /tmp/cpy-26/bin/python /tmp/unladen/bin/python
### normal_startup ###
Min: 0.219186 -> 0.352075: 1.6063x slower
Avg: 0.227228 -> 0.364384: 1.6036x slower
Significant (t=-51.879098, a=0.95)
Stddev: 0.00762 -> 0.02532: 3.3227x larger
Timeline: http://tinyurl.com/yfe8z3r
### startup_nosite ###
Min: 0.105949 -> 0.264912: 2.5004x slower
Avg: 0.107574 -> 0.267505: 2.4867x slower
Significant (t=-703.557403, a=0.95)
Stddev: 0.00214 -> 0.00240: 1.1209x larger
Timeline: http://tinyurl.com/yajn8fa
### bzr_startup ###
Min: 0.067990 -> 0.097985: 1.4412x slower
Avg: 0.084322 -> 0.111348: 1.3205x slower
Significant (t=-37.432534, a=0.95)
Stddev: 0.00793 -> 0.00643: 1.2330x smaller
Timeline: http://tinyurl.com/ybdm537
### hg_startup ###
Min: 0.016997 -> 0.024997: 1.4707x slower
Avg: 0.026990 -> 0.036772: 1.3625x slower
Significant (t=-53.104502, a=0.95)
Stddev: 0.00406 -> 0.00417: 1.0273x larger
Timeline: http://tinyurl.com/ycout8m
`bzr_startup` 和 `hg_startup` 分别衡量 Bazaar 和 Mercurial 显示其帮助屏幕所需的时间。`startup_nosite` 多次运行 `python -S`;`-S` 选项的使用很少见,但我们认为这能很好地指示启动时间增加的来源。
Unladen Swallow 在优化启动时间方面取得了进展,但仍有更多工作要做,还有待实现的进一步优化。提高启动时间是 Unladen Swallow 合并清单中的高优先级项目 [33]。
二进制大小
静态链接 LLVM 的代码生成、分析和优化库会显著增加 `python` 二进制文件的大小。下表报告了剥离后的磁盘二进制文件大小;为了更好地对应系统软件包管理器使用的配置,我们对二进制文件进行了剥离。我们认为这是任何二进制文件大小变化的更现实的衡量标准。
二进制大小 | CPython 2.6.4 | CPython 3.1.1 | Unladen Swallow r1041 |
---|---|---|---|
32 位 | 1.3MB | 1.4MB | 12MB |
64 位 | 1.6MB | 1.6MB | 12MB |
二进制文件大小的增加是由 LLVM 的代码生成、分析和优化库静态链接到 `python` 二进制文件中引起的。这可以通过修改 LLVM 以更好地支持共享链接,然后使用它来替代当前的静态链接来轻松解决。但目前,静态链接提供了链接 LLVM 成本的准确视图。
即使在静态链接的情况下,我们认为通过缩小 Unladen Swallow 对 LLVM 的依赖,仍有改进磁盘二进制文件大小的空间。这个问题正在积极解决中 [32]。
性能回顾
Unladen Swallow 的初始目标是比 CPython 2.6 提高 5 倍的性能。我们没有达到那个目标,更直白地说,甚至没有接近。为什么该项目未能达到目标,LLVM 风格的 JIT 是否 ever 能达到那个目标?
为什么 Unladen Swallow 未能实现其 5 倍目标?主要原因是 LLVM 所需的工作量超出了我们最初的预期。考虑到 Apple 正在发布基于 LLVM 的产品 [81],并且其他高级语言已成功实现了基于 LLVM 的 JIT([61]、[62]、[82]),我们曾假设 LLVM 的 JIT 相对来说没有致命的 bug。
事实证明这是不正确的。我们不得不将注意力从性能转移出来,去修复 LLVM JIT 基础设施中的一些关键 bug(例如,[83]、[84]),以及一些有利于沿着各种方向进行进一步优化的“锦上添花”的改进(例如,[86]、[85]、[87])。LLVM 的静态代码生成功能、工具和优化过程是稳定且经过严格测试的,但即时基础设施相对未经测试且存在 bug。我们已经修复了这一点。
(我们的假设是,我们之所以遇到这些问题——其他项目已经避免了的问题——是因为 CPython 的标准库测试套件的复杂性和彻底性。)
我们还把工程精力从性能转移到了支持工具上,如 gdb 和 oProfile。gdb 与 JIT 编译器配合得并不好,而 LLVM 之前与 oProfile 没有任何集成。拥有 JIT 感知的调试器和分析器对项目非常有价值,我们不后悔将时间投入到这些方向。有关更多信息,请参阅 调试 和 分析 部分。
基于 LLVM 的 CPython JIT 是否 ever 能达到 5 倍性能目标?基于 JIT 的 JavaScript 实现的基准测试结果表明 5 倍是可能的,PyPy 的 JIT 为数值工作负载带来的结果也表明了这一点。Self-92 [52] 的经验也很有启发性。
LLVM 能否实现这一点?我们相信我们才刚刚开始触及我们基于 LLVM 的 JIT 所能实现的潜力的表面。到目前为止,我们已将此系统整合的优化工作取得了显著成果(例如,[88]、[89]、[90])。我们迄今为止的经验是,Unladen Swallow 性能的限制因素在于实现文献所需的工程周期。我们发现 LLVM 易于使用和修改,其内置的优化大大简化了实现 Python 级优化的任务。
更多性能机会的概述在 未来工作 部分进行了讨论。
正确性和兼容性
Unladen Swallow 的正确性测试套件包括 CPython 的测试套件(位于 `Lib/test/` 下),以及许多重要的第三方应用程序和库 [6]。下表复制了这些应用程序和库的完整列表。这些包所需的任何依赖项,例如 `zope.interface` [34],作为测试主要包的一部分也被间接测试,从而扩大了被测试的第三方 Python 代码的语料库。
- 2to3
- Cheetah
- cvs2svn
- Django
- Nose
- NumPy
- PyCrypto
- pyOpenSSL
- PyXML
- Setuptools
- SQLAlchemy
- SWIG
- SymPy
- Twisted
- ZODB
这些应用程序在 Unladen Swallow 下运行时通过所有相关测试。请注意,一些在我们的 CPython 2.6.4 基线版本中失败的测试已被禁用,同样被禁用的还有那些假设 CPython 内部细节(如确切的字节码数字或字节码格式)的测试。任何有禁用测试的包都包含一个 `README.unladen` 文件,其中详细说明了更改(例如,[37])。
此外,Unladen Swallow 还在一系列内部 Google Python 库和应用程序上进行了自动测试。这些包括 Google 内部的 BigTable Python 绑定 [35]、Mondrian 代码审查应用程序 [36] 以及 Google 的 Python 标准库等。为使这些项目在 Unladen Swallow 下运行所需的更改一直归为以下三类之一:
- 添加 CPython 2.6 C API 兼容性。由于 Google 内部仍主要使用 CPython 2.4,我们需要将 `int` 的使用转换为 `Py_ssize_t` 以及类似的 API 更改。
- 修复或禁用对 CPython 版本号的显式、不正确的测试。
- 有条件地禁用那些针对 CPython 2.4 中已修复的 bug 进行规避或依赖这些 bug 的代码。
通过对如此广泛的公共和专有应用程序及库进行测试,我们确保了 Unladen Swallow 的正确性。测试暴露了我们已纠正的 bug。我们的自动化回归测试机制让我们在不断推进的过程中对我们的更改充满信心。
除了第三方测试,我们还向 CPython 的测试套件添加了更多针对语言或实现中我们认为未被测试或不明确的边缘情况的测试(例如,[48]、[49])。当实现优化时,这些测试尤其重要,它们有助于确保我们没有意外破坏 Python 的“黑暗角落”。
我们还构建了一个专门针对基于 LLVM 的 JIT 编译器及其优化措施的测试套件 [38]。由于编写优化编译器所固有的复杂性和微妙性,我们试图穷尽枚举我们正在编译和优化的构造、场景和边缘情况。JIT 测试还包括对 JIT 热度模型等内容的测试,使未来的 CPython 开发人员更容易维护和改进。
我们最近开始使用模糊测试(fuzz testing)[39] 来对编译器进行压力测试。过去我们使用了 pyfuzz [40] 和 Fusil [41],我们建议将它们作为 CPython 测试过程的自动化部分引入。
已知不兼容项
我们所知的与 Unladen Swallow 不兼容但与 CPython 2.6.4 兼容的唯一应用程序或库是 Psyco [64]。我们知道有些库,例如 PyGame [78],与 CPython 2.6.4 配合良好,但由于 Unladen Swallow 中的更改而性能有所下降。我们正在跟踪这个问题 [47] 并努力解决这些性能下降的情况。
虽然 Unladen Swallow 与 CPython 2.6.4 在源代码上兼容,但与二进制不兼容。针对其中一个编译的 C 扩展模块需要重新编译才能与另一个配合使用。
Unladen Swallow 的合并对 WPython 等长期的 CPython 优化分支的影响应该很小。WPython [104] 和 Unladen Swallow 在很大程度上是正交的,没有技术原因表明两者不能同时合并到 CPython 中。使 WPython 与增强了 JIT 的 CPython 版本兼容所需的更改应该非常小 [113]。对于其他 CPython 优化项目(例如,[114])也应该如此。
Stackless Python [115] 等侵入性分支更难以支持。由于 Stackless 极不可能被合并到 CPython [116],并且维护负担的增加是任何分支的固有部分,我们认为与 Stackless 的兼容性优先级相对较低。JIT 编译的堆栈帧使用 C 堆栈,因此 Stackless 应该能够像对待扩展模块的调用一样对待它们。如果事实证明这种情况不可接受,Stackless 可以移除 JIT 编译器或改进 JIT 代码生成以更好地支持基于堆的堆栈帧 [117]、[118]。
平台支持
Unladen Swallow 本质上受 LLVM 提供的平台支持限制,尤其是 LLVM 的 JIT 编译系统 [7]。LLVM 的 JIT 在 x86 和 x86-64 系统上支持最好,Unladen Swallow 在这些平台上接受的测试最多。我们对 LLVM/Unladen Swallow 的 x86 和 x86-64 硬件支持充满信心。PPC 和 ARM 支持存在,但使用不广泛,并且可能存在 bug(例如,[99]、[83]、[100])。
Unladen Swallow 已知在以下操作系统上运行:Linux、Darwin、Windows。Unladen Swallow 在 Linux 和 Darwin 上接受的测试最多,但它仍然在 Windows 上构建并能通过测试。
为了支持 LLVM JIT 不起作用的硬件和软件平台,Unladen Swallow 提供了 `--without-llvm` 的 `./configure` 选项。此标志会移除 Unladen Swallow 中任何依赖 LLVM 的部分,从而生成一个正常工作并通过测试的 Python 二进制文件,但没有性能优势。此配置推荐用于 LLVM 不支持的硬件,或内存使用比性能更重要的系统。
对 CPython 开发的影响
尝试修改 Python 或 CPython 字节码
Unladen Swallow 的 JIT 编译器在 CPython 字节码上运行,因此,它不受仅影响解析器的 Python 语言更改的影响。
我们建议将对 CPython 字节码编译器或单个字节码语义的更改首先在解释器循环中进行原型开发,然后在语义清晰后移植到 JIT 编译器。为了方便这一点,Unladen Swallow 包含了一个 `--without-llvm` 的配置时选项,它会移除 JIT 编译器及所有相关基础设施。这使得实验的负担与以前一样低,开发人员可以在当前易于入门的解释器循环中进行原型开发。
Unladen Swallow 开始实现其 JIT 编译器时,直接进行了从字节码实现到 LLVM API 调用的简单、朴素的翻译。我们发现这个过程很容易理解,并推荐同样的方法用于 CPython。我们在此包含 Unladen Swallow 存储库中的几处示例更改,作为这种开发风格的示例:[26]、[27]、[28]、[29]。
调试
Unladen Swallow 团队对 gdb 进行了修改,使其更容易使用 gdb 来调试 JIT 编译的 Python 代码。这些更改已在 gdb 7.0 中发布 [17]。它们使得 gdb 能够识别并展开 JIT 生成的堆栈帧。这使得 gdb 在 CPython 开发中能够继续正常工作,例如,当用户修改 `list` 类型或内置函数时。
我们更改后的示例堆栈回溯,其中 `baz`、`bar` 和 `foo` 是 JIT 编译的
Program received signal SIGSEGV, Segmentation fault.
0x00002aaaabe7d1a8 in baz ()
(gdb) bt
#0 0x00002aaaabe7d1a8 in baz ()
#1 0x00002aaaabe7d12c in bar ()
#2 0x00002aaaabe7d0aa in foo ()
#3 0x00002aaaabe7d02c in main ()
#4 0x0000000000b870a2 in llvm::JIT::runFunction (this=0x1405b70, F=0x14024e0, ArgValues=...)
at /home/rnk/llvm-gdb/lib/ExecutionEngine/JIT/JIT.cpp:395
#5 0x0000000000baa4c5 in llvm::ExecutionEngine::runFunctionAsMain
(this=0x1405b70, Fn=0x14024e0, argv=..., envp=0x7fffffffe3c0)
at /home/rnk/llvm-gdb/lib/ExecutionEngine/ExecutionEngine.cpp:377
#6 0x00000000007ebd52 in main (argc=2, argv=0x7fffffffe3a8,
envp=0x7fffffffe3c0) at /home/rnk/llvm-gdb/tools/lli/lli.cpp:208
以前,JIT 编译的帧会导致 gdb 错误地展开,生成大量明显错误的 ` #6 0x00002aaaabe7d0aa in ?? ()` 风格的堆栈帧。
亮点
- gdb 7.0 能够正确解析 JIT 编译的堆栈帧,允许在非 JIT 编译的函数(即 CPython 代码库的大部分)上完全使用 gdb。
- 在 JIT 编译的堆栈帧内部反汇编会自动打印构成该函数的指令列表。这比我们工作之前的 gdb 状态有所改进:开发人员需要猜测函数的起始地址并手动反汇编汇编代码。
- 灵活的底层机制允许 CPython 添加越来越多的信息,并最终在 gdb 对 JIT 编译的机器码的支持方面达到与 C/C++ 相提并论的水平。
低点
- gdb 无法打印局部变量,也无法告知您当前在 JIT 编译的函数中执行到哪一行。它也无法单步执行 JIT 编译的代码,只能一次执行一条指令。
- 尚未与 Apple 的 gdb 或 Microsoft 的 Visual Studio 调试器集成。
Unladen Swallow 团队正在与 Apple 合作,将这些更改纳入其未来的 gdb 版本。
分析
Unladen Swallow 与 oProfile 0.9.4 及更高版本集成 [18],以支持 Linux 系统上的汇编级分析。这意味着 oProfile 将在报告中正确地符号化 JIT 编译的函数。
示例报告,其中以 `#u#` 开头的符号名称是 JIT 编译的 Python 函数
$ opreport -l ./python | less
CPU: Core 2, speed 1600 MHz (estimated)
Counted CPU_CLK_UNHALTED events (Clock cycles when not halted) with a unit mask of 0x00 (Unhalted core cycles) count 100000
samples % image name symbol name
79589 4.2329 python PyString_FromFormatV
62971 3.3491 python PyEval_EvalCodeEx
62713 3.3354 python tupledealloc
57071 3.0353 python _PyEval_CallFunction
50009 2.6597 24532.jo #u#force_unicode
47468 2.5246 python PyUnicodeUCS2_Decode
45829 2.4374 python PyFrame_New
45173 2.4025 python lookdict_string
43082 2.2913 python PyType_IsSubtype
39763 2.1148 24532.jo #u#render5
38145 2.0287 python _PyType_Lookup
37643 2.0020 python PyObject_GC_UnTrack
37105 1.9734 python frame_dealloc
36849 1.9598 python PyEval_EvalFrame
35630 1.8950 24532.jo #u#resolve
33313 1.7717 python PyObject_IsInstance
33208 1.7662 python PyDict_GetItem
33168 1.7640 python PyTuple_New
30458 1.6199 python PyCFunction_NewEx
此支持是可用的,但尚未完善。Unladen Swallow 维护着一份清单,其中包含我们认为对改进 oProfile 集成至关重要的项目,以使其对核心 CPython 开发人员更有用 [19]。
亮点
- 在 Linux 上的 oProfile 中,JIT 编译帧的符号化功能正常。
低点
- 尚未投入精力来改进 Apple 的 Shark [20] 或 Microsoft 的 Visual Studio 分析工具对 JIT 编译帧的符号化。
- oProfile 输出仍需进一步完善。
我们建议使用 oProfile 0.9.5(及更高版本)来解决 x86-64 平台上 oProfile 中一个现已修复的 bug。然而,oProfile 0.9.4 在 32 位平台上也能正常工作。
考虑到将 oProfile 与 LLVM [21] 和 Unladen Swallow [22] 集成的简便性,其他分析工具也应该很简单,前提是它们支持类似的 JIT 接口 [23]。
我们记录了使用 oProfile 分析 Unladen Swallow 的过程 [24]。此文档将在合并时并入 CPython 的 `Doc/` 目录。
向 CPython 添加 C++
为了使用 LLVM,Unladen Swallow 在核心 CPython 树和构建过程中引入了 C++。这是依赖 LLVM 不可避免的一部分;虽然 LLVM 提供了 C API [8],但它功能有限,并未暴露 CPython 所需的功能。因此,我们使用 C++ 实现 Unladen Swallow JIT 及其支持基础设施的内部细节。我们不建议将整个 CPython 代码库转换为 C++。
亮点
- 可以轻松使用 LLVM 的完整、强大的代码生成及相关 API。
- 方便、抽象的数据结构简化了代码。
- C++ 仅限于 CPython 代码库中相对较小的部分。
- 可以通过 `./configure --without-llvm` 禁用 C++,甚至可以省略对 `libstdc++` 的依赖。
低点
管理 LLVM 发布版,C++ API 更改
LLVM 定期每六个月发布一次。这意味着在 CPython 3.x 版本开发过程中,LLVM 可能会发布两到三次。每个 LLVM 版本都带来了更新、更强大的优化、改进的平台支持和更复杂的代码生成。
LLVM 的发布通常包含不兼容的 LLVM C++ API 更改;LLVM 2.6 的发行说明 [9] 包含了一系列故意引入的不兼容性。Unladen Swallow 在开发过程中一直密切关注 LLVM Trunk。我们的经验是,LLVM API 的更改是显而易见的,并且可以轻松或机械地修复。我们在此引用 Unladen Swallow 树中的两项此类更改作为参考:[10]、[11]。
由于 API 不兼容,我们建议基于 LLVM 的 CPython 同时只兼容一个 LLVM 版本。这将降低核心开发团队的负担。从打包的角度来看,固定到一个 LLVM 版本应该不成问题,因为在 LLVM 发布后,预编译的 LLVM 包通常会通过标准的系统包管理器相当快地可用,如果不行,llvm.org 本身也提供了二进制发行版。
Unladen Swallow 历史上在其树中包含了一份 LLVM 和 Clang 的源代码副本;这是为了让我们在修改 LLVM Trunk 时能够密切跟踪它。我们不建议 CPython 采用这种开发模式。CPython 发布应该基于官方 LLVM 发布。预编译的 LLVM 包可从 MacPorts [12](用于 Darwin)以及大多数主要的 Linux 发行版([13]、[14]、[16])获得。LLVM 本身还提供其他二进制文件,例如 MinGW [25]。
LLVM 目前设计为静态链接;这意味着 CPython 的二进制发行版将包含 LLVM 的相关部分(不是全部!)。如上所述,这将增加二进制文件的大小。为简化下游的包管理,我们将修改 LLVM 以更好地支持共享链接。此问题将阻碍最终合并 [97]。
Unladen Swallow 已指派一名全职工程师负责在 LLVM 2.7 版本发布之前修复 LLVM 中所有剩余的关键问题。我们认为 CPython 3.x 能够依赖一个已发布的 LLVM 版本而不是像 Unladen Swallow 那样密切跟踪 LLVM Trunk 至关重要。我们相信我们将在 2010 年 5 月发布的 LLVM 2.7 版本之前完成这项工作 [98]。
构建 CPython
除了对 LLVM 的运行时依赖外,Unladen Swallow 还包含对 Clang [5] 的构建时依赖,Clang 是一个基于 LLVM 的 C/C++ 编译器。我们使用它来编译 C 语言 Python 运行时的一部分到 LLVM 的中间表示;这允许我们执行跨语言内联,从而提高性能。Clang 不是运行 Unladen Swallow 所必需的。Clang 的二进制包可从大多数主要的 Linux 发行版获得(例如,[15])。
我们检查了 Unladen Swallow 对 Python 构建时间的影响,包括配置、完整构建以及修改单个 C 源文件后的增量构建。
./configure | CPython 2.6.4 | CPython 3.1.1 | Unladen Swallow r988 |
---|---|---|---|
第一次运行 | 0m20.795s | 0m16.558s | 0m15.477s |
第二次运行 | 0m15.255s | 0m16.349s | 0m15.391s |
第三次运行 | 0m15.228s | 0m16.299s | 0m15.528s |
完全 make | CPython 2.6.4 | CPython 3.1.1 | Unladen Swallow r988 |
---|---|---|---|
第一次运行 | 1m30.776s | 1m22.367s | 1m54.053s |
第二次运行 | 1m21.374s | 1m22.064s | 1m49.448s |
第三次运行 | 1m22.047s | 1m23.645s | 1m49.305s |
完整的构建之所以受到影响,是因为 a) 需要额外的 `.cc` 文件与 LLVM 交互,b) LLVM 被静态链接到 `libpython`,c) Python 运行时的一部分被编译为 LLVM IR 以实现跨语言内联。
增量构建也比主线 CPython 稍慢。下表显示了修改 `Objects/listobject.c` 后的增量重建时间。
增量 make | CPython 2.6.4 | CPython 3.1.1 | Unladen Swallow r1024 |
---|---|---|---|
第一次运行 | 0m1.854s | 0m1.456s | 0m6.680s |
第二次运行 | 0m1.437s | 0m1.442s | 0m5.310s |
第三次运行 | 0m1.440s | 0m1.425s | 0m7.639s |
与完整构建一样,这段额外的时间来自于 LLVM 静态链接到 `libpython`。如果 `libpython` 与 LLVM 动态链接,这个开销会降低。
建议的合并计划
我们提议将我们的工作重点放在最终合并到 CPython 的 3.x 开发线。BDFL 已表明 2.7 将是 CPython 2.x 开发线的最后一个版本 [30],而且由于 2.7 Alpha 1 已发布,我们已经错过了时机。Python 3 是未来,我们将把我们的性能工作目标定在那里。
我们建议采用以下计划将 Unladen Swallow 合并到 CPython 源代码树中:
- 在 CPython SVN 仓库中创建一个分支用于工作,暂时命名为 `py3k-jit`。这将是 CPython `py3k` 分支的一个分支。
- 我们将使该分支与 `py3k` 保持紧密集成。分歧越大,我们的工作就越困难。
- 任何与 JIT 相关的补丁都将进入 `py3k-jit` 分支。
- 与 JIT 无关的补丁(在审查和批准后)将进入 `py3k` 分支,并合并回 `py3k-jit` 分支。
- 潜在的有争议的问题,例如引入新的命令行标志或环境变量,将在 python-dev 上讨论。
由于 Google 内部使用 CPython 2.x,Unladen Swallow 基于 CPython 2.6。我们需要将我们的编译器移植到 Python 3;这将在补丁应用到 `py3k-jit` 分支时进行,从而使该分支始终保持 Python 3 的一致实现。
我们相信这种方法对 3.2 或 3.3 的发布过程的干扰最小,同时我们能解决所有剩余的阻碍最终合并到 `py3k` 的问题。Unladen Swallow 维护着一份在最终合并前所需的已知问题清单 [31],其中包含本 PEP 中提到的所有问题;我们相信 CPython 社区会有自己的顾虑。这份清单并非一成不变;未来可能会出现其他阻碍最终合并到 `py3k` 分支的问题。
更改将直接提交到 `py3k-jit` 分支,只有大型、棘手或有争议的更改才会进行预提交代码审查。
应急计划
有可能我们无法将内存使用量或启动时间降低到 CPython 社区满意的水平。在这种情况下,我们的主要应急计划是将在线即时编译策略转变为离线提前编译策略,使用经过仪器化的 CPython 解释器循环来获取反馈。这与 GCC 的反馈定向优化(`-fprofile-generate`)[111] 和 Microsoft Visual Studio 的配置文件引导优化 [112] 使用的模型相同;我们在此将其称为“反馈定向优化”,或 FDO。
我们认为 Python 的 FDO 编译器不如 JIT 编译器。FDO 需要一套高质量、具有代表性的基准测试套件,这在开源和闭源开发中都相对稀少。JIT 编译器可以动态地查找和优化任何应用程序的“热点”——无论是否包含在基准测试套件中——从而使其能够自适应地应对应用程序瓶颈的变化,而无需人工干预。
如果需要一个提前编译的 FDO 编译器,它应该能够利用为 Unladen Swallow 的 JIT 编译器已经开发的大部分代码和基础设施。事实上,这两种编译策略可以并存。
未来工作
JIT 编译器是一个极其灵活的工具,我们远未穷尽其全部潜力。Unladen Swallow 维护着一份尚未实现的性能优化列表 [50],团队尚未有时间完全实现。示例:
- Python/Python 内联 [66]。我们的编译器目前不执行纯 Python 函数之间的内联。这项工作正在进行中 [68]。
- 拆箱(Unboxing)[67]。拆箱对于数值性能至关重要。PyPy 特别证明了拆箱对高度数值化工作负载的价值。
- 重新编译,适应。Unladen Swallow 目前只编译一次 Python 函数,基于其到目前为止的使用模式。如果使用模式发生变化,LLVM 的限制 [72] 会阻止我们重新编译该函数以更好地适应新的使用模式。
- JIT 编译正则表达式。现代 JavaScript 引擎会重用其 JIT 编译基础设施来提高正则表达式性能 [73]。Unladen Swallow 开发了 Python 正则表达式性能的基准测试([74]、[75]、[76]),但正则表达式性能的工作仍处于早期阶段 [77]。
- 跟踪编译 [91]、[92]。基于 PyPy 和 Tracemonkey [93] 的结果,我们认为 CPython JIT 应该在一定程度上包含跟踪编译。我们最初避免了纯粹的跟踪 JIT 编译器,而是选择了一个更简单的、一次一个函数的编译器。然而,这个一次一个函数的编译器为未来用相同方式实现的跟踪编译器奠定了基础。
- 配置文件生成/重用。JIT 收集的运行时数据可以持久化到磁盘,并供后续的 JIT 编译或外部工具(如 Cython [101] 或增强型代码覆盖率工具)重用。
这个列表绝不是详尽无遗的。存在大量关于动态语言优化的文献,这些优化可以也应该以 Unladen Swallow 基于 LLVM 的 JIT 编译器的形式来实现 [54]。
Unladen Swallow 社区
我们要感谢为 Unladen Swallow 做出贡献的开发者社区,特别是:James Abbatiello、Joerg Blank、Eric Christopher、Alex Gaynor、Chris Lattner、Nick Lewycky、Evan Phoenix 和 Thomas Wouters。
许可
根据 Google 与 PSF 的贡献者许可协议(CLA)的总体框架,Unladen Swallow 的所有工作都已根据 Python 软件基金会许可证 v2 [56] 的条款授权给 Python 软件基金会(PSF)。
LLVM 在《伊利诺伊大学/NCSA 开源许可证》[58] 下获得许可 [57],这是一个宽松的、OSI 批准的许可证。伊利诺伊大学香槟分校是 LLVM 的唯一版权持有人。
参考资料
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-3146.rst
最后修改: 2025-01-30 01:22:16 GMT