跟踪优化器

用户程序的跟踪不会直接转换为机器码。优化器模块实现了多种不同的语义保持转换,这些转换要么允许从跟踪中删除操作,要么将它们转换为需要更少时间或空间的操作。

优化器位于 rpython/jit/metainterp/optimizeopt/ 中。当您尝试理解此模块时,此页面可能会帮助您入门。

在详细解释某些优化之前,了解跟踪的外观至关重要。优化器带有一个测试套件。它包含许多跟踪示例,您可能需要查看它(在 rpython/jit/metainterp/optimizeopt/test/*.py 中)。允许的操作可以在 rpython/jit/metainterp/resoperation.py 中找到。这是一个跟踪示例

[p0,i0,i1]
label(p0, i0, i1)
i2 = getarrayitem_raw(p0, i0, descr=<Array Signed>)
i3 = int_add(i1,i2)
i4 = int_add(i0,1)
i5 = int_le(i4, 100) # lower-or-equal
guard_true(i5)
jump(p0, i4, i3)

一开始阅读起来可能很笨拙,但当您开始比较构建跟踪的 Python 代码时,它就会变得有意义

from array import array
a = array('i', range(101))
sum = 0; i = 0
while i <= 100: # can be seen as label
    sum += a[i]
    i += 1
    # jumps back to the while header

有更好的方法来计算 [0..100] 的总和,但它比 sum(range(101)) 更直观地说明了跟踪是如何构建的。请注意,跟踪语法是在测试套件中使用的语法。它也与 PYPYLOG 在运行时打印的跟踪非常相似。第一行给出输入变量,第二行是 label 操作,最后一行是向后的 jump 操作。

前面提到的这些指令是特殊的

  • 输入定义进入跟踪的输入参数类型和名称。
  • labeljump 可以定位到的指令。Label 指令具有关联的 JitCellToken,该令牌唯一标识该标签。任何跳转都具有标签的目标令牌。

该令牌保存在指令的所谓 描述符 中。它没有显式写入,因为测试中也没有这样做。但是测试套件为每个跟踪创建一个虚拟令牌,并将其作为描述符添加到 labeljump 中。当然,优化器在运行时也会这样做,但使用的是真实的值。示例跟踪在 getarrayitem_raw 中包含描述符。这里它注释了数组的类型。它是一个有符号整数数组。

高级概述

在 JIT 后端将任何跟踪转换为机器码之前,它会尝试将跟踪转换为执行速度更快的等效跟踪。方法 optimize_trace(位于 rpython/jit/metainterp/optimizeopt/__init__.py 中)是主要入口点。

优化按顺序依次应用,基本顺序如下

intbounds:rewrite:virtualize:string:earlyforce:pure:heap:unroll

每个冒号分隔的名称都附加了一个类,该类继承自 Optimization 类。Optimizer 类本身也派生自 Optimization 类,并实现优化控制逻辑。大多数优化只需要一次向前传递。跟踪使用 propagate_forward 方法“传播”到每个优化中。逐条指令,它从第一个优化流到最后一个优化。emit_operation 方法被调用用于传递到下一个优化器的每个操作。

intbounds 优化(主要关注优化整数操作)主要使用 基于规则的模式匹配 DSL 实现。

常见模式

要查找潜在的优化目标,有必要了解指令类型。简单的解决方案是使用操作编号(=类型)进行切换

for op in operations:
    if op.getopnum() == rop.INT_ADD:
        # handle this instruction
        pass
    elif op.getopnum() == rop.INT_FLOOR_DIV:
        pass
    # and many more

如果您开始匹配参数(第一个参数是常量,第二个参数是变量,反之亦然?),情况会变得更糟。解决此代码膨胀的模式是使用 make_dispatcher_method 将其移动到单独的方法中。它将方法与指令类型相关联

class OptX(Optimization):
    def prefix_INT_ADD(self, op):
        pass # emit, transform, ...

dispatch_opt = make_dispatcher_method(OptX, 'prefix_',
                                      default=OptX.emit_operation)
OptX.propagate_forward = dispatch_opt

optX = OptX()
for op in operations:
    optX.propagate_forward(op)

propagate_forward 搜索能够处理指令类型的方法。例如,INT_ADD 将调用 prefix_INT_ADD。如果没有该指令的函数,它将路由到默认实现(在此示例中为 emit_operation)。

重写优化

第二个优化称为“重写”,通常也称为强度削弱。一个简单的例子是,一个整数乘以 2 等于将位左移一次(例如 x * 2 == x << 1)。此优化不仅执行强度削弱,还执行布尔或算术简化。其他示例包括:x & 0 == 0x - 0 == x

每当遇到此类操作时(例如 y = x & 0),不会发出任何操作。相反,变量 y 被设为等于 0(= make_constant_int(op, 0))。在跟踪中找到的变量是可以在 rpython/jit/metainterp/history.py 中找到的类的实例。当一个值被设为等于另一个值时,它的盒会被设置为指向另一个值。

纯优化

“纯”优化交织在基本优化器中。它保存操作、结果、参数以使其具有纯语义。

这里的“纯”与 jit.elidable 装饰器相同:没有“可观察的”副作用且是引用透明的(操作可以用其结果替换而不会更改程序语义)。在 resoperation.py 中标记为 ALWAYS_PURE 的操作是 NOSIDEEFFECT 操作的子集。诸如 new、new array、getfield_(raw/gc) 等操作被标记为 NOSIDEEFFECT,但未标记为 ALWAYS_PURE。

纯操作以两种不同的方式进行优化。如果其参数是常量,则删除该操作并将结果转换为常量。如果不是,我们仍然可以使用记忆化技术:如果稍后我们再次看到对相同参数的相同操作,则无需重新计算其结果,而只需重用先前操作的结果。

展开优化

可以在文档 PyPy 的跟踪 JIT 中的循环感知优化 中找到详细说明

此优化不属于仅一次向前传递的传统方案。简而言之,它_一次_展开跟踪,连接这两个跟踪(通过将参数插入剥离跟踪的跳转和标签中),并使用信息来消除分配、传播常量并执行“optimizeopt”模块中当前存在的任何其他优化。

它位于所有优化之前,因此扩展了 Optimizer 类并在继续之前展开一次循环。

矢量化

本文档缺少哪些内容

  • 未解释保护
  • 未解释几种优化