PEP 611 – 百万限制
- 作者:
- Mark Shannon <mark at hotpy.org>
- 状态:
- 已撤回
- 类型:
- 标准跟踪
- 创建时间:
- 2019-12-05
- 发布历史:
摘要
此 PR 提出对 Python 代码及其实现的各个方面设置一个一百万 (1 000 000) 的软限制,以及一个更大的硬限制。
Python 语言没有为其许多功能指定限制。对于这些值不设置任何限制似乎至少表面上增强了程序员的自由,但在实践中,CPython VM 和其他 Python 虚拟机具有隐式限制,或者被迫假设限制是天文数字,这很昂贵。
此 PR 列出了几个将被限制为一百万的功能。
对于 CPython,硬限制将为八百万 (8 000 000)。
动机
虚拟机中需要表示许多值。如果未为这些值指定限制,则表示方式必须是低效的或容易溢出的。CPython 虚拟机使用 32 位值表示行号、堆栈偏移量和指令偏移量等值。这既低效又潜在不安全。
它低效是因为实际值很少需要超过十几个位来表示。
它不安全是因为恶意或生成不良的代码会导致值超过 232。
例如,行号在内部用 32 位值表示。考虑到模块很少超过几千行,这很低效。尽管效率低下,但它仍然容易溢出,因为攻击者很容易创建包含数十亿个换行符的模块。
内存访问通常是现代 CPU 性能的限制因素。更好地打包数据结构可以提高局部性并减少内存带宽,同时略微增加 ALU 使用率(用于移位和掩码)。能够安全地将重要值存储在 20 位中将允许在包括但不限于以下几个数据结构中节省内存
- 帧对象
- 对象头
- 代码对象
还可能存在更有效的指令格式,从而加快解释器调度。
这是一个有价值的权衡吗?
任何形式的限制的缺点是它可能会让某人的工作变得更难,例如,编写一个将模块大小保持在一百万行的代码生成器可能更难。但是,根据作者(曾编写过许多代码生成器)的意见,这种限制在实践中极不可能成为问题。
这些限制的优点是它为运行时实现者(无论是 CPython、PyPy 还是任何其他实现)提供了提高性能的自由。作者相信,即使是全球运行 Python 程序成本降低 0.1% 的潜在价值也将远远超过修改少数代码生成器的成本。
理由
对模块中的代码行数和局部变量数等值施加限制对虚拟机实现的简便性和效率有很大优势。如果限制足够大,就不会对语言用户产生负面影响。
通过为这些值选择一个固定但较大的限制,可以同时实现安全性和效率,同时不会给人类程序员带来不便,并且只会给代码生成器带来很少的问题。
一百万
值“一百万”很容易记住。
一百万的限制主要是对人类生成的代码的限制,而不是运行时大小。
单个模块中的一百万行代码是一个荒谬的代码集中度;整个 Python 标准库约为一百万行的 2/3,分布在 1600 个文件中。
Java 虚拟机 (JVM) [1] 为许多类似于此处涵盖的程序元素指定了 216-1 (65535) 的限制。此限制使有限的值能够以 16 位的形式存储,这是一种非常高效的机器表示。但是,在实践中,代码生成器很容易超过此限制,作者知道现有的 Python 代码已经超过了 216 行代码。
八百万的硬限制适合 23 位,虽然不像机器表示那样方便,但仍然相当紧凑。八百万的限制足够小以实现效率优势(只有 23 位),但足够大不会影响用户(没有人编写过这么大的模块)。
虽然生成的代码可能会超过限制,但代码生成器可以轻松地修改其输出以符合限制。作者在生成 Java 代码时至少遇到了两次 JVM 中的 64K 限制。解决方法相对简单,如果限制为一百万个字节码或代码行,则根本不需要这些解决方法。
如果需要,可以为超出百万限制的程序增加软限制。
设置一百万的软限制可以提供对问题代码的警告,而不会导致错误并强制立即修复。它还允许动态优化器使用更紧凑的格式而无需内联检查。
规范
此 PR 提出以下语言功能和运行时值具有百万的软限制。
- 模块中的源代码行数
- 代码对象中的字节码指令数。
- 代码对象的局部变量和堆栈使用量的总和。
- 运行时解释器中的类数。
- Python 代码的递归深度。
类数达到一百万之前,内存限制很可能是限制因素。
递归深度
递归深度限制仅适用于纯 Python 代码。用外语(例如 C)编写的代码可能会消耗硬件堆栈,因此递归深度限制在几千。如果硬件堆栈接近其限制,则预计实现将引发异常。对于混合使用 Python 和 C 调用的代码,硬件限制很可能首先生效。硬件递归的大小可能会在运行时变化,并且不可见。
软限制和硬限制
实现应在每次超过软限制时发出警告,除非硬限制与软限制的值相同。如果超过硬限制,则应引发异常。
取决于实现,可能会应用不同的硬限制。在某些情况下,硬限制可能低于软限制。例如,许多 micropython 端口不太可能支持如此大的限制。
检查和修改限制
将在 sys
模块中提供一个或多个函数,以便在运行时检查或修改软限制,但限制可能不会超过硬限制。
推断限制
这些限制不是规范的一部分,但可以从代码对象中字节码指令数的限制推断出少于一百万的限制。因为没有足够的指令来加载超过一百万个常量或使用超过一百万个名称。
- 代码对象中不同的名称数量。
- 代码对象中的常量数量。
对 CPython 施加这些限制的优势
模块中的代码行和代码对象限制。
将源代码编译为字节码或修改字节码以进行概要分析或调试时,需要中间形式。通过将操作数限制为 23 位,可以将指令以紧凑的 64 位形式表示,从而允许对指令序列进行非常快的传递。
具有 23 位操作数(相对分支为 24 位)允许指令适合 32 位,而无需额外的 EXTENDED_ARG
指令。这改进了调度,因为操作数严格地局限于指令。目前尚不清楚这是否有助于性能,这仅仅是一个可能的例子。
限制模块中的代码行数的好处主要是字节码的隐式限制。对于实现而言,代码对象每指令而不是模块每行被限制为一百万更为重要,但更容易解释一百万行的限制。具有统一的百万限制更容易记住。虽然不能保证,但很可能首先达到行限制,因此为开发人员提供更易于理解的错误消息。
运行时解释器中的总类数
此限制有可能显着减小对象头的尺寸。
目前,对象具有两个字头(对于没有引用的对象(int、float、str 等))或四个字头(对于有引用的对象)。通过减少最大类数,类引用的空间可以从 64 位减少到不到 32 位,从而允许更紧凑的头。
例如,超紧凑的头格式可能如下所示
struct header {
uint32_t gc_flags:6; /* Needs finalisation, might be part of a cycle, etc. */
uint32_t class_id:26; /* Can be efficiently mapped to address by ensuring suitable alignment of classes */
uint32_t refcount; /* Limited memory or saturating */
}
此格式将在 64 位机器上将没有插槽的 Python 对象的大小从 40 字节减少到 16 字节。
请注意,在 64 位机器上使用 32 位引用计数有两种方法。一种是将每个子解释器的内存限制为 32Gb。另一种是使用饱和引用计数,这会稍微慢一点,但允许无限内存分配。
实施
Python 实现不必强制执行这些限制。但是,如果可以强制执行限制而不影响性能,那么就应该执行。
预计 CPython 将按以下方式强制执行这些限制
- 模块中的源代码行数:版本 3.9 及更高版本。
- 代码对象中的字节码指令数:3.9 及更高版本。
- 代码对象的局部变量和堆栈使用量的总和:3.9 及更高版本。
- 运行时解释器中的类数:可能从 3.10 版本开始,也许在 3.9 版本中发出警告。
CPython 中的硬限制
CPython 将对所有上述值强制执行硬限制。硬限制的值将为 800 万。
假设一些机器生成的代码可能超过上述一个或多个限制。作者认为这是极不可能的,并且可以通过修改代码生成器的输出阶段轻松解决。
我们希望尽快从上述限制中获得性能提升。为此,CPython 将从版本 3.9 开始应用限制。为了简化过渡并最大程度地减少中断,初始限制将为 1600 万,在以后的版本中将降至 800 万。
向后兼容性
CPython 强制的实际硬限制将为
版本 | 硬限制 |
---|---|
3.9 | 1600 万 |
3.10 及更高版本 | 800 万 |
鉴于超过一百万限制的代码生成器很少见,以及它们通常使用的环境,如果任何受限制的数量超过一百万,则从 3.9 版本开始发出警告似乎是合理的。
历史上递归限制被设置为 1000。为了避免破坏隐式依赖于较小值的代码,软递归限制将逐步增加,如下所示
版本 | 软限制 |
---|---|
3.9 | 4 000 |
3.10 | 16 000 |
3.11 | 64 000 |
3.12 | 125 000 |
3.13 | 100 万 |
硬限制将立即设置为 800 万。
其他实现
除 CPython 之外的 Python 实现有不同的用途,因此可能需要不同的限制。只要明确记录这些限制,这是可以接受的。
通用实现
通用实现,例如 PyPy,应使用一百万的限制。如果最大兼容性是目标,那么它们也应该遵循 CPython 从 3.9 到 3.11 的行为。
专用实现
专用实现可以使用较低的限制,只要它们被明确记录。例如,专为嵌入式系统设计的实现,例如 MicroPython,可能会将限制设置为几千。
安全影响
最小化。这会减少任何 Python 虚拟机的攻击面,尽管幅度很小。
参考实现
目前没有。这将在 PEP 被接受后在 CPython 中实现。
被拒绝的想法
Tal Einat 建议在编译时能够向上修改硬限制。由于当前的 232 限制一直没有问题,并且允许 220 到 232 之间的限制的实际优势与支持此功能的额外代码复杂性相比似乎微不足道,因此该建议被拒绝了。
未解决的问题
目前没有。
参考文献
版权
本文件归属公共领域或 CC0-1.0-Universal 许可,以更具包容性的许可为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0611.rst