Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 撤回

随着 Unladen Swallow 走向挪威蓝鹦鹉的命运 [1] [2],此 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 能够更快,以便将其用于更多项目。谷歌就是其中一家公司。

Unladen Swallow 是谷歌赞助的 CPython 分支,旨在提升谷歌众多 Python 库、工具和应用程序的性能。为了使 Unladen Swallow 的采用尽可能简单,该项目最初的目标是四个方面

  • 对于单线程代码,性能提升到 CPython 2.6.4 基线水平的 5 倍。
  • 与有效的 CPython 2.6 应用程序 100% 源代码兼容。
  • 与有效的 CPython 2.6 C 扩展模块 100% 源代码兼容。
  • 设计用于最终合并回 CPython。

我们选择 2.6.4 作为我们的基线,因为谷歌在内部使用 CPython 2.4,而直接从 CPython 2.4 跳到 CPython 3.x 被认为是不可行的。

为了达到所需的性能,Unladen Swallow 实现了遵循 Urs Hoelzle 在 Self 上工作的传统的即时 (JIT) 编译器 [51],在运行时收集反馈并利用它来告知编译时优化。这类似于当前一代 JavaScript 引擎 [59][60];大多数 Java 虚拟机 [64];Rubinius [61],MacRuby [63] 和其他 Ruby 实现;Psyco [65];以及其他。

我们明确拒绝任何认为我们的想法是原创的建议。我们尽力在任何可能的情况下重用其他研究人员的公开成果。如果我们做出了任何原创性的工作,那都是偶然的。我们尽可能地从学术界和产业界的各个角落汲取好的想法。在 Unladen Swallow wiki [54] 上可以找到部分启发 Unladen Swallow 的研究论文列表。

关于优化动态语言的关键观察是,它们在理论上才是动态的;在实践中,每个单独的函数或代码片段都是相对静态的,使用一组稳定的类型和子函数。当前的 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) [95] 来工作,同时考虑来自 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 [102] 和 Shedskin [103] 都是 Python 的静态编译器。我们认为这些是针对 CPython 历史上糟糕的性能的有用但有限的解决方法。Shedskin 不支持完整的 Python 标准库 [104],而 Cython 则需要手动添加 Cython 特定的注释以获得最佳性能。

    像这样的静态编译器对于编写扩展模块而不必担心引用计数很有用,但由于它们是静态的、提前编译器,因此它们无法优化即时编译器在运行时数据告知下考虑的全部代码范围。

  • IronPython:IronPython [107] 是在 Microsoft 的 .Net 平台上的 Python。它没有在 Mono [108] 上积极测试,这意味着它基本上只能在 Windows 上使用,因此不适合作为通用的 CPython 替代品。
  • Jython:Jython [109] 是 Python 2.5 的完整实现,但明显比 Unladen Swallow 慢(根据测量基准测试慢 3-5 倍),并且不支持 CPython 扩展模块 [110],这使得大型应用程序的迁移成本过高。
  • Psyco:Psyco [65] 是一个针对 CPython 的专门 JIT 编译器,实现为一个扩展模块。它主要提高数值代码的性能。优点:已存在;使某些代码运行更快。缺点:仅限 32 位,没有 64 位支持计划;仅支持 x86;非常难以维护;由于对齐问题,与 SSE2 优化的代码不兼容。
  • PyPy:PyPy [66] 在数值代码方面具有良好的性能,但在某些工作负载下比 Unladen Swallow 慢。从 CPython 迁移大型应用程序到 PyPy 成本过高:PyPy 的 JIT 编译器仅支持 32 位 x86 代码生成;重要的模块,如 MySQLdb 和 pycrypto,无法在 PyPy 上构建;PyPy 没有提供嵌入式 API,更不用说与 CPython 相同的 API 了。
  • PyV8:PyV8 [111] 是一款处于 alpha 阶段的实验性 Python 到 JavaScript 编译器,运行在 V8 之上。PyV8 并未实现完整的 Python 语言,并且不支持 CPython 扩展模块。
  • WPython:WPython [105] 是对 CPython 解释器循环的基于词代码的重新实现。虽然它对解释器性能进行了适度的改进 [106],但它并不是即时编译器的替代品。解释器永远无法像优化的机器代码那样快。我们认为 WPython 和类似的解释器增强功能是对我们工作的补充,而不是竞争对手。

性能

基准测试

Unladen Swallow 开发了一套相当庞大的基准测试套件,范围从旨在测试单个功能的合成微基准测试到整个应用程序的宏基准测试。这些基准测试的灵感分别来自第三方贡献者(例如 html5lib 基准测试)、Google 自身的内部工作负载(slowspitfirepickleunpickle),以及在更广泛的 Python 社区中大量使用的工具和库(django2to3spambayes)。这些基准测试通过一个名为 perf.py 的单个接口运行,该接口负责收集内存使用信息、绘制性能图表以及对基准测试结果运行统计数据以确保显著性。

完整的可用基准测试列表可在 Unladen Swallow wiki 上找到 [43],其中包括关于自行下载和运行基准测试的说明。我们所有的基准测试都是开源的;没有任何 Google 专有。我们相信,这套基准测试可以作为衡量任何完整 Python 实现的有用工具,事实上,PyPy 已经在使用这些基准测试进行自己的性能测试 [81][96]。我们欢迎这种做法,并希望从 Python 社区获得更多基准测试工作负载。

我们一直致力于收集宏基准测试和尽可能模拟真实应用程序的基准测试,当无法运行整个应用程序时。在另一个维度上,我们的基准测试集合最初侧重于 Google 的 Python 代码(Web 应用、文本处理)所看到的各种工作负载,尽管此后我们扩展了集合以包含 Google 不关心的工作负载。到目前为止,我们一直回避大量数值工作负载,因为 NumPy [80] 已经在这些代码上做得非常出色,因此提高数值性能并不是团队的初始优先事项;我们已开始将此类基准测试纳入集合 [97],并已开始优化数值 Python 代码。

除了这些基准测试之外,还有一些我们明确不感兴趣的基准测试工作负载。Unladen Swallow 专注于提高纯 Python 代码的性能,因此 NumPy 等扩展模块的性能并不重要,因为 NumPy 的核心例程是用 C 实现的。同样,我们认为涉及大量 IO 的工作负载,如 GUI、数据库或套接字密集型应用程序,无法准确衡量解释器或代码生成优化。也就是说,当然有空间可以提高标准库中 C 语言扩展模块的性能,因此,我们添加了 cPicklere 模块的基准测试。

与 CPython 2.6.4 的性能对比

下面的图表比较了 CPython 2.6.4 和 Unladen Swallow 多次基准测试迭代的算术平均值。perf.py 收集的数据比这多,事实上,算术平均值并非全部内容;为了简洁起见,我们只复制了平均值。我们包含了来自学生双尾 T 检验 [44] 在 95% 置信区间内的 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 函数编译为机器代码。例如,这导致了 html5librietveld 基准测试的时间线图表中看到的行为,并降低了 2to3 的整体性能。我们有一个活跃的开发分支来解决这个问题([46][47]),但在 CPython 当前线程系统的限制下工作使过程变得复杂,并且需要比最初预期的更多关注和时间。我们认为这个问题对于最终合并到 py3k 分支至关重要。

我们显然没有达到最初设定的 5 倍性能提升的目标。接下来是 性能回顾,其中说明了我们未能达到初始性能目标的原因。我们维护着一个尚未实现的性能工作列表 [50]

内存使用

下表显示了 Unladen Swallow 的每个默认基准测试在 CPython 2.6.4 和 Unladen Swallow r988 中的最大内存使用量(以千字节为单位),以及基准测试生命周期内的内存使用量时间线。我们包含了 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++静态初始化器也会增加启动时间,预编译的C运行时例程的集合(我们希望内联到Python代码中)也会增加启动时间。

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_startuphg_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.3M 1.4M 12M
64 位 1.6M 1.6M 12M

二进制文件大小增加是由将LLVM的代码生成、分析和优化库静态链接到python二进制文件中引起的。这可以通过修改LLVM以更好地支持共享链接,然后使用它而不是当前的静态链接来直接解决。但是,目前,静态链接提供了对链接到LLVM的成本的准确了解。

即使在静态链接的情况下,我们也相信仍然有空间通过缩小Unladen Swallow对LLVM的依赖来改进磁盘上的二进制文件大小。这个问题正在积极解决[32]

性能回顾

我们最初的目标是Unladen Swallow的性能比CPython 2.6提高5倍。我们没有达到这个目标,坦率地说,甚至没有接近。为什么该项目没有达到该目标,基于LLVM的JIT能否达到该目标?

为什么Unladen Swallow没有达到其5倍的目标?主要原因是LLVM需要比我们最初预期的更多工作。基于Apple正在基于LLVM发布产品[82],以及其他高级语言已成功实现了基于LLVM的JIT([61][63][83]),我们假设LLVM的JIT相对没有阻碍性错误。

事实证明这是不正确的。我们不得不将注意力从性能转移到修复LLVM的JIT基础架构中的一些关键错误(例如,[84][85]),以及一些不错的增强功能,这些增强功能将能够沿着各个轴进一步优化(例如,[87][86][88])。LLVM的静态代码生成工具、工具和优化过程是稳定且经过压力测试的,但即时基础架构则相对未经测试且存在错误。我们已经修复了这个问题。

(我们的假设是,我们遇到了这些问题——其他项目避免的问题——是因为CPython的标准库测试套件的复杂性和彻底性。)

我们还将工程工作从性能转移到支持工具,如gdb和oProfile。gdb根本无法很好地与JIT编译器一起使用,并且LLVM以前没有与oProfile集成。拥有JIT感知的调试器和分析器对项目非常有价值,我们不后悔将时间投入到这些方向。有关更多信息,请参阅调试分析部分。

基于LLVM的CPython JIT能否达到5倍的性能目标?基于JIT的JavaScript实现的基准测试结果表明,5倍确实是可能的,PyPy的JIT为数值工作负载提供的结果也证明了这一点。Self-92的经验[52]也具有启发意义。

LLVM能否实现这一点?我们相信,我们才刚刚开始了解基于LLVM的JIT能够提供的功能。我们迄今已纳入此系统的优化已取得重大成果(例如,[89][90][91])。我们迄今的经验是,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中现已修复的错误的代码。

针对这一广泛的公共和专有应用程序和库进行测试,对于确保Unladen Swallow的正确性至关重要。测试发现了我们已予以纠正的错误。在我们前进的过程中,我们的自动化回归测试机制让我们对我们的更改充满信心。

除了第三方测试之外,我们还为CPython的测试套件添加了更多测试,以针对我们认为未经测试或未充分指定的语言或实现的极端情况(例如,[48][49])。在实施优化时,这些测试尤其重要,有助于确保我们没有意外破坏Python的较暗角落。

我们还构建了一个专门针对基于LLVM的JIT编译器及其为其实现的优化的测试套件[38]。由于编写优化编译器固有的复杂性和微妙性,我们试图穷举列出我们正在编译和优化的结构、场景和极端情况。JIT测试还包括JIT热度模型的测试,这使得未来的CPython开发人员更容易维护和改进。

我们最近开始使用模糊测试[39]来对编译器进行压力测试。我们过去使用过pyfuzz[40]和Fusil[41],我们建议将它们作为CPython测试过程的自动化部分引入。

已知的不兼容性

我们知道唯一一个在Unladen Swallow下无法工作但在CPython 2.6.4下可以工作的应用程序或库是Psyco[65]。我们知道一些库,例如PyGame[79],它们在CPython 2.6.4下运行良好,但在Unladen Swallow中进行的更改导致其性能有所下降。我们正在跟踪此问题[47],并努力解决这些性能下降的实例。

虽然Unladen Swallow与CPython 2.6.4源代码兼容,但它与之不兼容二进制文件。针对其中一个编译的C扩展模块需要重新编译才能与另一个一起使用。

Unladen Swallow的合并对WPython等长期存在的CPython优化分支的影响应该很小。WPython[105]和Unladen Swallow在很大程度上是正交的,并且没有技术原因阻止两者都合并到CPython中。使WPython与JIT增强的CPython版本兼容所需的更改应该很少[114]。对于其他CPython优化项目(例如,[115])也应该是如此。

侵入式分支的CPython,如Stackless Python[116],支持起来更具挑战性。由于Stackless不太可能合并到CPython[117],并且任何分支的维护负担都会增加,因此我们认为与Stackless的兼容性是相对较低优先级的。JIT编译的栈帧使用C栈,因此Stackless应该能够像对待通过扩展模块的调用一样对待它们。如果事实证明这是不可接受的,Stackless可以删除JIT编译器或改进JIT代码生成以更好地支持基于堆的栈帧[118][119]

平台支持

Unladen Swallow 本身受到 LLVM 提供的平台支持的限制,特别是 LLVM 的 JIT 编译系统 [7]。LLVM 的 JIT 在 x86 和 x86-64 系统上支持最好,并且这些平台也是 Unladen Swallow 接受最多测试的平台。我们对 LLVM/Unladen Swallow 对 x86 和 x86-64 硬件的支持充满信心。PPC 和 ARM 支持也存在,但使用不广泛,并且可能存在错误(例如,[100][84][101])。

已知 Unladen Swallow 可以在以下操作系统上运行:Linux、Darwin、Windows。Unladen Swallow 在 Linux 和 Darwin 上接受了最多的测试,尽管它仍然可以在 Windows 上构建并通过测试。

为了支持 LLVM 的 JIT 不起作用的硬件和软件平台,Unladen Swallow 提供了一个 ./configure --without-llvm 选项。此标志会移除 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 类型或内置函数。

更改后的回溯示例,其中 bazbarfoo 是 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]

亮点

  • 在 oProfile 上对 Linux 上的 JIT 帧进行符号化。

不足之处

  • 尚未投入工作来改进 Apple 的 Shark [20] 或 Microsoft 的 Visual Studio 分析工具对 JIT 编译的帧的符号化。
  • oProfile 输出仍需要一些改进。

我们建议使用 oProfile 0.9.5(及更高版本)来解决 oProfile 中 x86-64 平台上的一个已修复的错误。但是,oProfile 0.9.4 在 32 位平台上可以正常工作。

鉴于将 oProfile 与 LLVM 集成非常容易 [21] 和 Unladen Swallow [22],其他分析工具也应该很容易,前提是它们支持类似的 JIT 接口 [23]

我们已经记录了使用 oProfile 分析 Unladen Swallow 的过程 [24]。此文档将在合并时合并到 CPython 的 Doc/ 树中。

在 CPython 中添加 C++

为了使用 LLVM,Unladen Swallow 已将 C++ 引入核心 CPython 树和构建过程中。这是依赖 LLVM 的不可避免的一部分;虽然 LLVM 提供了一个 C API [8],但它功能有限,无法公开 CPython 所需的功能。因此,我们已使用 C++ 实现 Unladen Swallow JIT 及其支持基础设施的内部细节。我们不建议将整个 CPython 代码库转换为 C++。

亮点

  • 轻松使用 LLVM 的完整、强大的代码生成和相关 API。
  • 方便的抽象数据结构简化了代码。
  • C++ 限于 CPython 代码库的相对较小的角落。
  • 可以通过 ./configure --without-llvm 禁用 C++,甚至省略对 libstdc++ 的依赖。

不足之处

  • 开发人员必须了解两种相关的语言,C 和 C++,才能处理 CPython 内部结构的全部范围。
  • 需要开发和实施 C++ 样式指南。PEP 7 将扩展 [120] 以包含 C++,方法是从 Unladen Swallow [70]、LLVM [71] 和 Google [72] 的 C++ 样式指南中提取相关部分。
  • 不同的 C++ 编译器会发出不同的 ABI;如果 CPython 使用一个 C++ 编译器编译,而扩展模块使用另一个 C++ 编译器编译,则可能会导致问题。

管理 LLVM 版本,C++ API 更改

LLVM 每六个月定期发布。这意味着在 CPython 3.x 版本的开发过程中,LLVM 可能会发布两到三次。每次 LLVM 发布都会带来更新、更强大的优化、改进的平台支持和更复杂的代码生成。

LLVM 版本通常包含对 LLVM C++ API 的不兼容更改;LLVM 2.6 的发行说明 [9] 包含一个有意引入的不兼容性列表。在开发过程中,Unladen Swallow 一直紧密跟踪 LLVM 主干。我们的经验是,LLVM API 更改是显而易见的,并且可以轻松或机械地解决。我们在此包含 Unladen Swallow 树中的两个此类更改作为参考:[10][11]

由于 API 不兼容性,我们建议基于 LLVM 的 CPython 目标每次与 LLVM 的单个版本兼容。这将降低核心开发团队的开销。从打包的角度来看,固定到 LLVM 版本不应成为问题,因为预构建的 LLVM 包通常在 LLVM 发布后很快就会通过标准系统包管理器提供,如果没有,llvm.org 本身也包含二进制版本。

Unladen Swallow 历史上在 Unladen Swallow 树中包含 LLVM 和 Clang 源代码树的副本;这样做是为了让我们在对其进行修补时能够紧密跟踪 LLVM 主干。我们不建议 CPython 使用这种开发模型。CPython 版本应基于官方的 LLVM 版本。预构建的 LLVM 包可从 MacPorts [12](适用于 Darwin)和大多数主要的 Linux 发行版([13][14][16])获取。LLVM 本身提供了其他二进制文件,例如适用于 MinGW 的二进制文件 [25]

LLVM 目前旨在进行静态链接;这意味着 CPython 的二进制版本将包含 LLVM 的相关部分(并非全部!)。这将增加二进制文件的大小,如上所述。为了简化下游包管理,我们将修改 LLVM 以更好地支持共享链接。此问题将阻止最终合并 [98]

Unladen Swallow 已指派一名全职工程师在 LLVM 2.7 发布之前修复 LLVM 中任何剩余的关键问题。我们认为 CPython 3.x 能够依赖 LLVM 的已发布版本至关重要,而不是像 Unladen Swallow 一样紧密跟踪 LLVM 主干。我们相信我们将在 LLVM 2.7 发布(预计在 2010 年 5 月)之前完成这项工作 [99]

构建 CPython

除了对 LLVM 的运行时依赖之外,Unladen Swallow 还包括对 Clang 的构建时依赖 [5],这是一个基于 LLVM 的 C/C++ 编译器。我们使用它将 C 语言 Python 运行时的部分编译为 LLVM 的中间表示;这使我们能够执行跨语言内联,从而提高性能。Clang 不是运行 Unladen Swallow 的必要条件。大多数主要的 Linux 发行版(例如,[15])都提供了 Clang 二进制包。

我们检查了 Unladen Swallow 对构建 Python 所需时间的影响,包括配置、完整构建以及修改单个 C 源文件后的增量构建。

./configure CPython 2.6.4 CPython 3.1.1 Unladen Swallow r988
运行 1 0m20.795s 0m16.558s 0m15.477s
运行 2 0m15.255s 0m16.349s 0m15.391s
运行 3

0分15.228秒 0分16.299秒 0分15.528秒
完全编译 CPython 2.6.4 CPython 3.1.1 Unladen Swallow r988
运行 1 1分30.776秒 1分22.367秒 1分54.053秒
运行 2 1分21.374秒 1分22.064秒 1分49.448秒
运行 3

1分22.047秒 1分23.645秒 1分49.305秒

完整构建时间受到以下因素的影响:a) LLVM交互需要额外的.cc文件,b) 将LLVM静态链接到libpython中,c) 将Python运行时的部分编译为LLVM IR以启用跨语言内联。

增量构建也比主线CPython略慢。下表显示了修改Objects/listobject.c后增量重建的时间。

增量编译 CPython 2.6.4 CPython 3.1.1 Unladen Swallow r1024
运行 1 0分1.854秒 0分1.456秒 0分6.680秒
运行 2 0分1.437秒 0分1.442秒 0分5.310秒
运行 3

0分1.440秒 0分1.425秒 0分7.639秒

与完整构建一样,额外的时间来自将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)[112]和Microsoft Visual Studio的配置文件引导优化[113]使用的模型相同;在这里,我们将将其称为“反馈导向优化”,或FDO。

我们认为,用于Python的FDO编译器将不如JIT编译器。FDO需要一套高质量、有代表性的基准测试套件,这在开源和闭源开发中都比较罕见。JIT编译器可以动态查找和优化任何应用程序中的热点——无论是否有基准测试套件——使其能够在无需人工干预的情况下适应应用程序瓶颈的变化。

如果需要提前的FDO编译器,它应该能够利用为Unladen Swallow的JIT编译器已经开发的大部分代码和基础设施。实际上,这两种编译策略可以并存。

未来的工作

JIT编译器是一个非常灵活的工具,我们还没有完全发挥其全部潜能。Unladen Swallow维护着一个尚未实现的性能优化的列表[50],团队还没有时间完全实现这些优化。示例

  • Python/Python内联[67]。我们的编译器目前不执行纯Python函数之间的内联。这方面的工作正在进行中[69]
  • 拆箱[68]。拆箱对于数值性能至关重要。特别是PyPy已经证明了拆箱对大量数值工作负载的价值。
  • 重新编译、适应。Unladen Swallow目前只编译一个Python函数一次,基于其到目前为止的使用模式。如果使用模式发生变化,LLVM的限制[73]阻止我们重新编译函数以更好地满足新的使用模式。
  • JIT编译正则表达式。现代JavaScript引擎重用它们的JIT编译基础设施来提高正则表达式的性能[74]。Unladen Swallow已经开发了Python正则表达式性能的基准测试([75][76][77]),但正则表达式性能方面的工作仍处于早期阶段[78]
  • 跟踪编译[92][93]。根据PyPy和Tracemonkey的结果[94],我们认为CPython JIT应该在某种程度上包含跟踪编译。我们最初避免了纯粹的跟踪JIT编译器,转而采用更简单的逐函数编译器。但是,这个逐函数编译器为将来使用相同术语实现的跟踪编译器奠定了基础。
  • 配置文件生成/重用。JIT收集的运行时数据可以持久化到磁盘,并在后续的JIT编译或外部工具(如Cython[102]或反馈增强代码覆盖率工具)中重用。

此列表绝非详尽无遗。关于动态语言优化的文献浩如烟海,这些优化应该用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。

许可证

所有关于Unladen Swallow的工作都根据Python软件基金会许可证v2[56]的条款许可给Python软件基金会(PSF),在Google与PSF的 blanket Contributor License Agreement 的保护伞下。

LLVM根据伊利诺伊大学/NCSA开源许可证[57][58]许可,这是一个自由的、OSI批准的许可证。伊利诺伊大学厄巴纳-香槟分校是LLVM的唯一版权持有者。

参考文献


来源:https://github.com/python/peps/blob/main/peps/pep-3146.rst

上次修改:2023-09-09 17:39:29 GMT