跳转至

Python 3.11 更快的 CPython

在 Python 3.11 的发布文档 What’s New In Python 3.11 中谈到 3.11 要比 3.10 快 10-60%。文中将性能的提升主要归功于两个方面:更快的启动和更快的运行时。

更快的启动

冻结导入 / 静态的 Code 对象

在 3.10 以及之前的版本,模块的加载过程为:

读取 __pycache__ -> 反序列化 -> 在堆上分配 Code 对象 -> 计算
而在 3.11 中,启动所需的核心模块被“冻结”,这意味着这些 Code 对象直接由解释器静态分配,新的加载过程为:
静态分配的 Code 对象 -> 计算
因此解释器的启动速度得到了 10-15% 的提升,这对于短时间运行的程序意义重大。

更快的运行时

开销更低、惰性的 Python 帧

调用一个 Python 函数的时候,解释器就会创建 Python 帧对象,它用于保存函数的执行信息,下面是几点针对它的优化:

  • 简化帧的创建过程。

  • 通过大量重用 C 堆栈上的帧空间来避免内存分配。

  • 简化内部帧的结构,只包含基本信息。之前的帧还保存了额外的调试和内存管理信息。

现在只有通过调试器或者 sys._getframeinspect.currentframe 等 Python 自省函数,才会创建以前结构的帧对象。对于大多数的用户代码,不会再被创建帧对象。因此几乎所有 Python 函数的调用速度都会有所提升。pyperformance 显示这个数据在 3-7%。

内联化(Inlined) Python 的函数调用

在调用 Python 函数期间,解释器会调用一个 C 的计算函数来解释 Python 函数的代码。这有效限制了纯 Python 递归以保证 C 栈的安全。

但在 3.11 中,当 CPython 解释器检测到 Python 代码调用另一个 Python 函数时,它会创建一个新的函数帧,并“jump”到新帧中的新代码。这避免了每次都调用 C 的解释函数。

现在大多数的 Python 函数调用不消耗 C 的堆栈空间,因而提高了速度。在一些简单的递归函数中,比如斐波拉契数列和阶乘,我们观察到了 1.7 倍的性能提升。同时这也意味着递归函数可以更深。通过 pyperformance 确认有 1-3% 的提升。

PEP 659 专用的自适应解释器

PEP 659 是 Faster CPython 项目的重点之一。一般而言,虽然 Python 是一种动态语言,但大多数代码都有一些对象和类型很少被更改。这个概念被称为类型稳定性(type stability)

在运行时,解释器将尝试在被执行的代码中寻找公共模式(common patterns)和类型稳定性。然后 Python 将用一个特化(specialized)操作替换当前的操作。这还引入了另一个称为内联缓存(inline caching)的概念,在这个概念中,解释器直接在 bytecode 层面缓存一些高成本操作的结果。

同时特化程序还会将某些常见的指令对组合成一个超指令,从而减少执行过程中的开销。

解释器只有在检测到“hot”(多次执行)的代码时才会进行特化。这样可以避免在 run-once 代码上浪费时间。在代码过于动态的时候,解释器也会去特化(de-specialize)。

操作 形式 特化 提升
二元运算 x + x x - x x * x int float str 等类型的二元运算(加、减、乘)使用 fast paths 直接操作其底层类型。 10%
下标访问 a[i] 通过下标访问 list tuple dict 等容器类型时,直接索引其底层数据结构。__getitem__() 也会被内联优化。 10-25%
给下标赋值 a[i] = z 同下标访问。 10-25%
调用 f(arg) C(arg) len str 等函数直接调用其底层的 C 函数。 20%
读取全局变量 print len globals 和 builtins 的对象被缓存,调用它们不再需要查找命名空间。 3.8 已有
读取属性 o.attr 类似于对全局对象的优化,大多数情况下读取属性也不再需要查找。 3.10 已有
读取方法然后调用 o.meth() 方法的实际地址也被缓存,即使是具有很长继承链的方法,也不再需要查找。 10-20%
给属性赋值 o.attr = z 同读取属性。 2% (pyperformance 压测)
序列解包 *seq 针对 list tuple 等公共容器的特化。 8%

杂项

  • 得益于惰性创建对象的命名空间,现在的对象需要的内存更少。

  • 在没有触发异常时,消除了 try 语句的成本。

  • 在解释器中更简洁地表示异常,将捕获异常所需的时间减少了大约 10% 。

  • re 模块的正则表达式匹配引擎进行了部分重构,在 pyperformance 的正则表达式压测 中显示了 10% 的性能提升。

官方问答

问:我应该如何编写代码来利用这些提升呢?

答:无需修改代码。遵循通用最佳实践编写 Pythonic 的代码。Faster CPython 项目会针对我们所观察到的公共代码的模式进行优化。

问:CPython 3.11 会使用更多的内存吗?

答:应该不会。我们预计内存使用量不会超过 3.10 的 20%。而且还可以通过上面提到的帧对象和对象字典的内存优化来消除。

问:为什么我在我的项目中没有感受到任何提升?

答:某些代码不会有明显的提升。比如你的代码将大部分时间花在 I/O 操作上,或者已经使用 numpy 这样的 C 扩展库来完成大部分的计算,那是不会有明显提升的。目前这些优化对纯 Python 代码的提升最大。
此外,pyperformance 的数据是一个几何平均值。即使是在 pyperformance 的基准测试中,也有些基准测试的速度略有放缓,但另外一些得到了接近 2 倍提升!

问:是否使用了 JIT 编译器?

答:没有,我们仍在探索其他的优化途径。