PyJitPl5

本文档描述了第五代 RPython JIT 生成器。

JIT 的实现

JIT 的 理论在原则上非常棒,但实际代码却另当别论。本节试图概述 RPython 的 JIT 是如何实现的。在深入研究源代码之前,了解 RPython 翻译工具链 的工作原理非常有帮助。

几乎所有 JIT 特定的代码都位于 rpython/jit 子目录中。翻译时期的代码位于 codewriter 目录中。metainterp 目录包含平台无关的代码,包括追踪器和优化器。backend 目录中的代码负责生成机器码。

JIT 提示

要向解释器添加 JIT,RPython 只需要在目标解释器中添加两个提示。它们是 jit_merge_point 和 can_enter_jit。jit_merge_point 应该放在操作码分发开始的地方。它允许 JIT 在运行机器码不再合适时回退到解释器。can_enter_jit 位于应用程序级循环的末尾。在 Python 解释器中,这是 JUMP_ABSOLUTE 字节码。Python 解释器在其默认解释器循环的几个重写方法中定义了其提示,位于 pypy/module/pypyjit/interp_jit.py 中。

希望使用 RPython JIT 生成器的解释器必须定义一个 绿色 变量列表和一个 红色 变量列表。绿色 变量是循环常量。它们用于识别当前循环。红色变量用于执行循环中使用的所有其他内容。例如,Python 解释器将代码对象和指令指针作为绿色变量传递,将帧对象和执行上下文作为红色变量传递。这些对象在 JIT 提示的位置传递给 JIT。

JIT 生成

在翻译的高级 Python 操作转换为后端低级操作的 RTyping 阶段之后,翻译驱动程序调用 metainterp/warmspot.py 中的 apply_jit() 以向当前正在翻译的解释器添加 JIT 编译器。apply_jit() 决定使用哪个汇编器后端,然后将其余工作委托给 WarmRunnerDesc 类。WarmRunnerDesc 在函数图中找到这两个 JIT 提示。它重写包含 jit_merge_point 提示的图(称为入口图),以便能够处理特殊的 JIT 异常,这些异常在退出 JIT 时向解释器指示特殊条件。can_enter_jit 提示的位置被替换为对一个函数的调用,即 warmstate.py 中的 maybe_compile_and_run,该函数检查当前循环是否“热”并且应该被编译。

接下来,从入口图开始,codewriter/*.py 将解释器的图转换为 JIT 字节码。由于此字节码存储在最终二进制文件中,因此它被设计为简洁而不是快速。字节码代码编写器不会“看到”(它看到的内容由 JIT 的策略定义)解释器的每个部分。在这些情况下,它只需插入一个不透明的调用。

最后,翻译完成,包括将解释器的字节码包含在最终二进制文件中,并且解释器已准备好使用 JIT 的运行时组件。

追踪

在启用了 JIT 的解释器上运行的应用程序代码正常启动;它在通常的评估循环之上进行解释。当应用程序循环关闭时(在 can_enter_jit 提示处),解释器调用 WarmEnterState 的 maybe_compile_and_run() 方法。此方法会递增与当前绿色变量关联的计数器。当此计数器达到某个级别时(通常表示应用程序循环已运行多次),JIT 进入追踪模式。

追踪 是 JIT 解释在翻译时生成的解释器解释应用程序级代码的字节码的地方。这使它能够看到构成应用程序级循环的确切操作。追踪由 metainterp/pyjitpl.py 中的 MetaInterp 和 MIFrame 类执行。maybe_compile_and_run() 创建一个 MetaInterp 并调用其 compile_and_run_once() 方法。这将为循环的输入参数(从 jit_merge_point 提示传递的红色和绿色变量)初始化 MIFrame,并将其设置为开始解释入口图的字节码。

在开始解释之前,循环输入参数将被包装在一个 中。盒(在 metainterp/history.py 中定义)包装 JIT 正在解释的程序中值的 value 和 type。盒主要有两种:常量盒和普通盒。常量盒用于在追踪期间假定已知的值。这些不一定是编译时常量。所有被“提升”的值(JIT 为优化目的假定为常量)也存储在常量盒中。普通盒包含在循环运行期间可能发生变化的值。普通盒有三种:BoxInt、BoxPtr 和 BoxFloat,常量盒有四种:ConstInt、ConstPtr、ConstFloat 和 ConstAddr。(ConstAddr 仅用于解决翻译工具链中的限制。)

元解释器开始解释 JIT 字节码。每个操作都执行,然后记录在一个操作列表中,称为追踪。操作可以有一个它们操作的盒列表,即参数。某些操作(如 GETFIELD 和 GETARRAYITEM)也具有描述其参数如何在内存中布局的特殊对象。追踪生成的全部可能操作列在 metainterp/resoperation.py 中。当在追踪期间发生对 JIT 具有字节码的函数(解释器级)调用时,另一个 MIFrame 会添加到堆栈中,并且追踪继续使用相同的历史记录。这会将跨越调用的操作列表展平。最重要的是,它会展开操作码分发循环。解释一直持续到看到 can_enter_jit 提示。此时,应用程序级循环的一个完整迭代已被看到并记录。

因为只记录了一个迭代,所以 JIT 仅了解循环中的一个代码路径。例如,如果有一个 if 语句构造如下

if x:
    do_something_exciting()
else:
    do_something_else()

并且当 JIT 执行追踪时 x 为真,则只有代码路径 do_something_exciting 将被添加到追踪中。在将来的运行中,为确保此路径仍然有效,一个称为 保护操作 的特殊操作将被添加到追踪中。保护是一个小的测试,用于检查 JIT 在追踪期间做出的假设是否仍然成立。在上面的示例中,将为 x 生成一个 GUARD_TRUE 保护,然后运行 do_something_exciting

一旦元解释器验证它已追踪了一个循环,它就会决定如何编译它所拥有的内容。在这些操作之间有一个可选的优化阶段,将在本页面的后面部分介绍。后端将追踪操作转换为特定机器的汇编代码。然后它将编译后的循环交回前端。下次在应用程序代码中看到循环时,可以运行优化的汇编代码而不是正常的解释器。

优化

JIT 使用多种新旧技术来加快机器代码的运行速度。

虚拟值和可虚拟化对象

虚拟 值是在循环期间创建的数组、结构体或 RPython 级实例,并且不会通过调用或超出循环的生命周期从循环中转义。由于它仅由 JIT 使用,因此可以“优化掉”;根本不需要分配该值,并且其字段可以存储为一等值,而不是在内存中取消引用它们。虚拟值允许解释器中的临时对象被展开。例如,只要已知对象不会转义机器代码,PyPy 解释器中的 W_IntObject 就可以被展开为仅包含其整数值。

可虚拟化对象 与虚拟值类似,因为其结构在机器代码中被优化掉了。但是,可虚拟化对象可以从 JIT 控制的代码中转义。

其他优化

JIT 的大部分优化器都包含在 metainterp/optimizeopt/ 子目录中。请参阅以了解更多详细信息。

更多资源

有关当前 JIT 的更多文档以第一篇已发表的文章的形式提供

Antonio Cuni 的博士论文 的第 5 章和第 6 章概述了追踪 JIT 的一般工作原理,并提供了更多有关 PyPy 的 JIT 的具体案例的信息。

带有 JIT 标签的 博客文章 也可能包含其他信息。