Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 611 – 一百万限制

作者:
Mark Shannon <mark at hotpy.org>
状态:
已撤回
类型:
标准跟踪
创建日期:
2019年12月5日
发布历史:


目录

摘要

本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标准库大约是三分之二百万行,分布在1600个文件中。

Java虚拟机 (JVM) [1] 为许多类似于此处涵盖的程序元素指定了 216-1 (65535) 的限制。此限制使得有限的值可以容纳在 16 位中,这是一种非常高效的机器表示。然而,这个限制在实践中很容易被代码生成器超出,作者知道现有的 Python 代码已经超过 216 行代码。

八百万的硬限制可容纳 23 位,虽然对于机器表示而言不那么方便,但仍然相当紧凑。八百万的限制足够小,以获得效率优势(仅 23 位),但足够大,不会影响用户(没有人写过如此大的模块)。

虽然生成的代码有可能超出限制,但代码生成器很容易修改其输出以符合要求。作者在生成 Java 代码时至少两次遇到 JVM 中的 64K 限制。解决方法相对简单,如果限制是一百万字节码或代码行,则无需这些解决方法。

如有必要,对于超出一百万限制的程序,可以提高软限制。

设置一百万的软限制可以警告有问题代码,而不会导致错误并强制立即修复。它还允许动态优化器使用更紧凑的格式而无需内联检查。

规范

本PR提议以下语言特性和运行时值具有一百万的软限制。

  • 模块中的源代码行数
  • 代码对象中的字节码指令数量。
  • 代码对象的局部变量和堆栈使用量的总和。
  • 正在运行的解释器中的类的数量。
  • Python 代码的递归深度。

内存限制很可能在类的数量达到一百万之前成为限制因素。

递归深度

递归深度限制仅适用于纯 Python 代码。用 C 等外语编写的代码可能会消耗硬件堆栈,因此递归深度限制在几千。预计当硬件堆栈接近其限制时,实现将引发异常。对于混合使用 Python 和 C 调用的代码,最有可能首先应用硬件限制。硬件递归的大小在运行时可能会有所不同,并且不可见。

软限制和硬限制

除非硬限制与软限制相同,否则实现应在超出软限制时发出警告。当超出硬限制时,应引发异常。

根据不同的实现,可能会应用不同的硬限制。在某些情况下,硬限制可能低于软限制。例如,许多微型 Python 端口不太可能支持如此大的限制。

自省和修改限制

`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 一百万

硬限制将立即设置为 800 万。

其他实现

除 CPython 以外的 Python 实现有不同的目的,因此可能需要不同的限制。这是可以接受的,只要限制明确文档化。

通用实现

通用实现,例如 PyPy,应使用一百万限制。如果最大兼容性是一个目标,那么它们也应遵循 CPython 在 3.9 到 3.11 版本的行为。

专用实现

专用实现可以使用较低的限制,只要它们明确地记录下来。例如,为嵌入式系统设计的实现(例如 MicroPython)可能会施加低至几千的限制。

安全隐患

最小化。这会少量减少任何Python虚拟机的攻击面。

参考实现

目前还没有。一旦 PEP 被接受,这将在 CPython 中实现。

被拒绝的想法

Tal Einat 建议在编译时向上修改硬限制。这被驳回了,因为当前的 232 限制一直不是问题,并且允许 220 到 232 之间的限制的实际优势与支持此功能的额外代码复杂性相比似乎微不足道。

未解决的问题

目前还没有。

参考资料

https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf


来源: https://github.com/python/peps/blob/main/peps/pep-0611.rst

最后修改: 2025-02-01 08:55:40 GMT