抽象
为了获得良好的性能,动态语言的虚拟机必须 将它们执行的代码专门化为 正在运行的程序。这种专业化通常与“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_map | 0 | 1 | 0 |
opcache/数据 | 0 | 4.8 | 4 |
总 | 2 | 7.8 | 6 |
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-通用许可证,以更宽松的许可证为准。
文章声明:以上内容(如有图片或视频在内)除非注明,否则均为腾龙猫勺儿原创文章,转载或复制请以超链接形式并注明出处。
本文作者:猫勺本文链接:https://www.jo6.cn/post/59.html
还没有评论,来说两句吧...