PEP 659 – 专业自适应口译员

猫勺猫勺 03-14 177 阅读 0 评论

抽象

为了获得良好的性能,动态语言的虚拟机必须 将它们执行的代码专门化为 正在运行的程序。这种专业化通常与“JIT”相关联 编译器,但即使没有机器代码生成也是有益的。

专业化、适应性强的口译员是投机性的专业口译员 根据当前正在操作的类型或值,并适应变化 在这些类型和值中。

专业化为我们提供了改进的性能,而适应性允许 解释器,当程序中的使用模式发生变化时快速更改, 限制因专业化错误而导致的额外工作量。

PEP 建议使用专门的、自适应的口译员 积极地编写代码,但在非常小的区域内,并且能够调整到 以低成本快速地进行错误专业化。

在 CPython 中添加一个专业的、自适应的解释器将带来显着的 性能改进。很难想出有意义的数字, 因为它很大程度上取决于基准和尚未发生的工作。 广泛的实验表明,速度最高可达 50%。 即使加速只有 25%,这仍然是一个值得的增强。

赋予动机

Python 被广泛认为速度慢。 虽然 Python 永远无法达到像 C 这样的低级语言的性能, fortran,甚至 java,我们希望它与快速竞争 脚本语言的实现,例如 V8 for Javascript 或 luajit for 路亚。 具体来说,我们希望通过 CPython 实现这些性能目标 使所有 Python 用户受益,包括那些无法使用 PyPy 或 其他备用虚拟机。

实现这些性能目标还有很长的路要走,并且需要大量的 工程努力,但我们可以通过以下方式朝着这些目标迈出重要一步 加快口译员的速度。 学术研究和实际实施都表明,快速 解释器是快速虚拟机的关键部分。

虚拟机的典型优化成本高昂,因此需要长时间的“预热” 需要时间才能确信优化成本是合理的。 为了快速加速,没有明显的预热时间, VM 应该推测,即使在几次之后,专业化也是合理的 函数的执行。为了有效地做到这一点,口译员必须能够 以非常便宜的方式持续优化和反优化。

通过使用自适应和推测专业化,粒度为 单个虚拟机指令, 我们得到了一个更快的解释器,它还可以生成分析信息 以便将来进行更复杂的优化。

理由

有许多实用的方法可以加速虚拟机的动态 语言。 然而,专业化是最重要的,无论是它本身还是作为一个 其他优化的推动者。 因此,首先将我们的精力集中在专业化上是有意义的, 如果我们想提高 CPython 的性能。

专用化通常是在 JIT 编译器的上下文中完成的, 但研究表明,口译员的专业化可以提高绩效 值得注意的是,甚至优于朴素的编译器[1]。

学术界已经提出了几种方法可以做到这一点 文献,但大多数尝试优化大于 a 单字节码 [1] [2]。 使用比单个指令更大的区域需要代码来处理 区域中间的去优化。 单个字节码级别的专业化使去优化 微不足道,因为它不能发生在一个区域的中间。

通过推测性地专门化单个字节码,我们可以获得显着的 性能改进,除了最本地化之外什么都没有, 而且实施起来微不足道,去优化。

文献中最接近这种 PEP 的方法是 “内联缓存遇上加速”[3]。 此 PEP 具有内联缓存的优点, 但增加了快速去优化使性能的能力 在专业化失败或不稳定的情况下更可靠。

性能

专业化的加速很难确定,因为许多专业化 依赖于其他优化。加速似乎在 10% - 60% 的范围内。

  • 大部分加速直接来自专业化。最大的 贡献者是属性查找、全局变量和调用的加速。

  • 一小部分但有用的部分来自改进的调度,例如 超级指令和通过加速实现的其他优化。

实现

概述

任何从专业化中受益的指令都将被 该指令的“适应性”形式。执行时,自适应指令 将专注于他们看到的类型和值。 这个过程被称为“加速”。

一旦代码对象中的指令执行了足够多的次数, 该指令将通过用新指令替换来“专业化” 预计该操作的执行速度会更快。

加快

加速是用更快的变体替换慢指令的过程。

与不可变字节码相比,快速代码具有许多优点:

  • 它可以在运行时更改。

  • 它可以使用跨越行并采用多个操作数的超级指令。

  • 它不需要处理跟踪,因为它可以回退到原始跟踪 字节码。

为了支持跟踪,加快了指令格式 应与不可变的、用户可见的字节码格式匹配: 16 位指令,其中 8 位操作码后跟 8 位操作数。

自适应指令

每个将从专业化中受益的指令都被替换为 加速期间的自适应版本。例如 该指令将替换为 。LOAD_ATTRLOAD_ATTR_ADAPTIVE

每个自适应指令都会定期尝试使自己专业化。

专业化

CPython 字节码包含许多表示高级的指令 运营,并将从专业化中受益。示例包括 、 和 。CALLLOAD_ATTRLOAD_GLOBALBINARY_ADD

通过为其中每个引入一个“系列”的专门说明 指令允许有效的专业化, 因为每个新指令都专用于单个任务。 每个家庭将包括一个“适应性”指令,该指令维护一个计数器 并尝试在该计数器达到零时进行专业化。

每个家庭还将包括一个或多个专门说明, 执行相当于通用操作的速度要快得多,前提是他们的 输入符合预期。 每个专门的指令将保持一个饱和计数器,该计数器将 每当输入符合预期时,就会递增。输入不应该 正如预期的那样,计数器将递减,泛型操作将减少 将执行。 如果计数器达到最小值,则指令将按以下方式进行反优化 只需将其操作码替换为自适应版本即可。

辅助数据

大多数专业说明系列将需要比 可以适应 8 位操作数。为此,请立即输入多个 16 位条目 按照说明用于存储此数据。这是内联的一种形式 cache,一个“内联数据缓存”。非专业或自适应指令将 使用此缓存的第一个条目作为计数器,只需跳过其他条目即可。

指令系列示例

LOAD_ATTR

该指令将对象的命名属性加载到堆栈的顶部, 然后将堆栈顶部的对象替换为属性。LOAD_ATTR

这显然是专业化的候选者。属性可能属于 普通实例、类、模块或许多其他特殊情况之一。

LOAD_ATTR最初会加快到哪个 会跟踪它的执行频率,并在执行足够多的次数时调用内部函数,或者跳转到原始指令来执行加载。优化时,种类 的属性将被检查,如果有合适的专业指令 被发现,它将就地替换。LOAD_ATTR_ADAPTIVE_Py_Specialize_LoadAttrLOAD_ATTRLOAD_ATTR_ADAPTIVE

专业化可能包括:LOAD_ATTR

  • LOAD_ATTR_INSTANCE_VALUE属性存储在 对象的值数组,并且不被覆盖的描述符遮挡。

  • LOAD_ATTR_MODULE从模块加载属性。

  • LOAD_ATTR_SLOT从其对象加载属性 类定义 。__slots__

请注意,这如何允许优化以补充其他优化。 它与用于 许多对象。LOAD_ATTR_INSTANCE_VALUE

LOAD_GLOBAL

该指令在全局命名空间中查找名称 然后,如果全局命名空间中不存在, 在 builtins 命名空间中查找它。 在 3.9 中,包含的 C 代码包含要检查的代码 是否应该修改整个代码对象以添加缓存, 无论是 global 命名空间还是 builtins 命名空间, 用于在缓存中查找值的代码,以及回退代码。 这使得它变得复杂和笨重。 它还执行许多冗余操作,即使据称经过优化也是如此。LOAD_GLOBALLOAD_GLOBAL

使用一系列指令使代码更易于维护和更快, 因为每条指令只需要处理一个问题。

专业化将包括:

  • LOAD_GLOBAL_ADAPTIVE将像上面一样操作。LOAD_ATTR_ADAPTIVE

  • LOAD_GLOBAL_MODULE可以专门用于值在 globals 命名空间。检查命名空间的键是否具有 不更改,它可以从存储的索引中加载值。

  • LOAD_GLOBAL_BUILTIN可以专门用于值为 在 builtins 命名空间中。它需要检查全局的键 命名空间尚未添加到,并且内置命名空间尚未添加到 改变。请注意,我们并不关心全局命名空间的值 已经改变了,只是钥匙。

注意

此 PEP 概述了管理专业化的机制,但未 指定要应用的特定优化。 细节,甚至整个实现,可能会发生变化 随着代码的进一步发展。

兼容

语言、库或 API 不会有任何变化。

用户能够检测到新事物存在的唯一方法 解释器是通过定时执行,使用调试工具, 或测量内存使用情况。

成本

内存使用情况

对于任何执行任何类型缓存的方案,一个明显的问题是 “它还使用多少内存?” 简短的回答是“没那么多”。

将内存使用情况与 3.10 进行比较

CPython 3.10 每条指令使用 2 个字节,直到执行计数 达到 ~2000 当它为每条指令分配另一个字节时,并且 每条指令 32 个字节,带有缓存 ( 和 )。LOAD_GLOBALLOAD_ATTR

下表显示了每条指令的额外字节数,以支持 3.10 opcache 或建议的自适应解释器,在 64 位机器上。

版本3.10 感冒3.10 热3.11
专业0%~15%~25%
法典2
2
2
opcache_map010
opcache/数据04.84
27.86

3.10 cold在代码达到 ~2000 限制之前。 显示达到阈值后的缓存使用情况。3.10 hot

相对内存使用量取决于有多少代码“热”到足以触发 在 3.10 中创建缓存。盈亏平衡点,即使用的内存 到 3.10 与 3.11 相同是 ~70%。

还值得注意的是,实际的字节码只是代码的一部分 对象。代码对象还包括名称、常量和相当多的 调试信息。

总之,对于大多数应用程序,其中许多功能是相对的 未使用,3.11 将比 3.10 消耗更多的内存,但不会消耗太多。

安全隐患

没有


被拒绝的想法

通过实现具有内联数据缓存的专用自适应解释器, 我们隐含地拒绝了许多优化 CPython 的替代方法。 但是,值得强调的是,一些想法,例如准时制 编译,没有被拒绝,只是推迟了。

在字节码之前存储数据缓存。

此 PEP 的早期实现用于 3.11 alpha 使用了不同的缓存 方案如下所述:

加速指令将存储在数组中(既没有必要,也不需要 希望将它们存储在 Python 对象中),其格式与 原始字节码。辅助数据将存储在单独的数组中。

每条指令将使用 0 个或多个数据条目。 族中的每条指令必须分配相同数量的数据, 尽管某些指令可能无法全部使用。 无法专用的指令,例如, 不需要任何条目。 实验表明,25%到30%的指令可以有效地专业化。 不同的家庭需要不同数量的数据, 但大多数需要 2 个条目(64 位机器上为 16 个字节)。POP_TOP

为了支持比 256 条指令更大的功能, 我们计算指令的第一个数据输入的偏移量 如。(instruction offset)//2 + (quickened operand)

与 Python 3.10 中的 opcache 相比,此设计:

更快;它不需要内存读取来计算偏移量。 3.10 需要两次读取,这是依赖的。

使用更少的内存,因为数据可以针对不同的大小 指令族,并且不需要额外的偏移量数组。 可以支持更大的功能,最多约 5000 条指令 每个函数。3.10可以支持1000左右。

我们拒绝了这个方案,因为内联缓存方法更快 而且更简单。

版权

文档被置于公共领域或位于 CC0-1.0-通用许可证,以更宽松的许可证为准。


The End 微信扫一扫

文章声明:以上内容(如有图片或视频在内)除非注明,否则均为腾龙猫勺儿原创文章,转载或复制请以超链接形式并注明出处。

本文作者:猫勺本文链接:https://www.jo6.cn/post/59.html

上一篇 下一篇

相关阅读

发表评论

访客 访客
快捷回复: 表情:
评论列表 (暂无评论,177人围观)

还没有评论,来说两句吧...

取消
微信二维码
微信二维码
支付宝二维码