码农高天
新版本通过连续栈内存替代堆分配帧,并以goto循环复用单层解释器函数,彻底削减了深层调用引发的C栈与缓存开销。
AI-assisted summary of 【python】3.11是如何变快的?我来给你讲一下其中原理! — original source. Check the original for full context.
Source published:
Key ideas
- Python 3.11 实现了官方基准测试平均约 25% 的性能跃升,其中以递归斐波那契或阶乘为代表的高频函数调用场景提速最高可达 70% 左右。 (0:00)
- 底层字节码完成统一重构,Python 3.11 废弃了旧有的 CALL_FUNCTION 字节码,全面整合为统一的 CALL 字节码并引入快速通道(Fast Path)。 (5:00)
- 全新的轻量级 _PyInterpreterFrame 替代了传统的 PyFrameObject,剔除非必须调试数据并采用专有连续内存栈分配,大幅提升了 CPU 缓存友好度。 (5:00)
- 在 Fast Path 优化下,Python 函数调用不再递归调用 C 级评估函数,而是通过 goto start_frame 重置上下文并在同一 C 栈帧内循环,消除了 C 栈递归膨胀开销。 (5:00)
Detailed notes
## Background 本视频由博主“码农高天”于2024年5月15日发布在YouTube平台上。视频简介指出,本期内容深入探讨了Python 3.11如何实现整体提速25%,重点解析了针对函数调用环节的核心优化机制。
## TL;DR 视频深入剖析了Python 3.11性能跃升的技术内幕,对比了3.10与3.11在函数调用底层实现上的根本差异。在Python 3.10中,函数调用依赖CALL_FUNCTION字节码,存在C调用栈递归膨胀与堆内存中PyFrameObject离散分配导致的性能瓶颈与缓存不友好问题。而Python 3.11通过引入统一的CALL字节码、轻量级且紧凑排布在连续栈内存中的_PyInterpreterFrame,以及利用goto start_frame避免C级递归的内联优化,极大抹平了调用开销并提升了CPU缓存命中率。
## Atmosphere & style 视频风格硬核严谨且直奔技术本质,主讲人以清晰从容的节奏拆解CPython底层C源码与字节码机制,充满资深开发者的探究感。从直观的斐波那契性能实测数据切入,再层层剥离复杂底层抽象,将枯燥的解释器内部运作剖析得井井有条、通俗透彻,给观众带来一种水到渠成、豁然开朗的硬核求知体验。
## Bulletin - [00:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=0s) Python 3.11 实现了官方基准测试平均约 25% 的性能跃升,其中以递归斐波那契或阶乘为代表的高频函数调用场景提速最高可达 70% 左右。 - [05:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=300s) 底层字节码完成统一重构,Python 3.11 废弃了旧有的 CALL_FUNCTION 字节码,全面整合为统一的 CALL 字节码并引入快速通道(Fast Path)。 - [05:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=300s) 全新的轻量级 _PyInterpreterFrame 替代了传统的 PyFrameObject,剔除非必须调试数据并采用专有连续内存栈分配,大幅提升了 CPU 缓存友好度。 - [05:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=300s) 在 Fast Path 优化下,Python 函数调用不再递归调用 C 级评估函数,而是通过 goto start_frame 重置上下文并在同一 C 栈帧内循环,消除了 C 栈递归膨胀开销。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=0s) Python 3.11 性能提升的核心抓手之一是对函数调用机制的优化,官方将其归因于 Lazy Frames 与 Inline Function Calls。 - [00:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=0s) 在基准测试对比中,计算 fib(24) 的耗时从 Python 3.10 的约 9 毫秒缩短至 Python 3.11 的 5 到 6 毫秒。 - [00:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=0s) Python 3.10 在执行 CALL_FUNCTION 时,依赖 _PyFunction_Vectorcall 和 _PyEval_Vector 等一系列 C 级递归调用。 - [00:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=0s) Python 3.10 每次函数调用都必须在堆(Heap)上动态分配庞大的 PyFrameObject,不仅开销高昂且包含大量运行时非必需的调试信息。 - [00:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=0s) 堆上零散分配的 PyFrameObject 导致内存碎片化且极不连续,无法有效利用现代 CPU 的缓存行(Cache Line),引发显著性能损失。 - [00:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=0s) Python 3.10 中每次 Python 级函数调用都会同步在 C 语言调用栈中累加深度,容易带来显著调用开销甚至触发 C 栈溢出(爆栈)。 - [05:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=300s) Python 3.11 将各类调用收拢为 CALL 字节码,并在针对常规 Python 函数调用时触发内联优化的 Fast Path。 - [05:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=300s) 轻量级结构体 _PyInterpreterFrame 剥离了厚重的调试数据,大幅精简了数据体积并加快了构建效率。 - [05:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=300s) Python 3.11 为解释器帧开辟了专有的连续内存栈(custom stack),使得上下文紧密排列,极大地改善了 CPU 缓存命中率。 - [05:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=300s) Fast Path 在完成必要计算后直接通过 goto start_frame 跳转回当前 _PyEval_EvalFrameDefault 开头,实现了 C 栈层面的内联复用。 - [05:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=300s) 单层 C 栈帧的循环复用机制彻底消除了底层 C 函数调用的额外开销,同时彻底解决了调用过深导致的 C 栈空间消耗问题。 - [05:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=300s) 内联优化仅适用于常规调用的快速通道,对于无法内联优化的复杂或边缘情况,CPython 依然保留传统回退(fallback)机制以保证兼容性。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=0s) Python 3.11 的性能跃升与 3.10 中的传统函数调用开销
Python 3.11 正式发布后,其最引人注目的亮点莫过于官方宣称的整体性能平均提升约 25%。在某些特定场景下,提速效果更为夸张。以一个极其简单的递归版斐波那契数列(Fibonacci)为例,在 Python 3.10 下计算第 24 项大约需要 9 毫秒,而到了 Python 3.11,执行时间骤减至 5 到 6 毫秒之间。根据 Python 官网的说明,这一跃升的核心原因之一在于对函数调用的优化——即 Lazy Frames(延迟帧求值)与 Inline Function Calls(内联函数调用)。由于递归版斐波那契数列和阶乘(Factorial)这类型代码的开销几乎全在函数调用上,官方甚至表示这类函数能获得高达 70% 左右的速度提升。码农高天在开场提到:“这个提升就可以说是非常之巨大的……在Python的官网上给出了Python3.11性能提升的几个核心原因,这里面的lazy frames跟inline function calls都是对python函数调用的速度提升。”
为了拆解优化的本质,首先需要了解 Python 3.10 是如何处理函数调用的。在 Python 3.10 中,执行调用会命中 `CALL_FUNCTION` 字节码。追踪其源码可见,底层通过一系列调用最终落到 `_PyFunction_Vectorcall` 函数中,随后进入 `_PyEval_Vector`。在 `_PyEval_Vector` 内部主要完成两件事:第一,从堆(heap)中分配内存,创建全新的 `PyFrameObject`;第二,调用 `_PyEval_EvalFrame`,而该函数又会递归地调用包含巨大 `switch` 语句的主解释器执行循环函数 `_PyEval_EvalFrameDefault`。在 3.11 之前,所有 Python 代码都在 `PyFrameObject` 中运行,该对象不仅容纳了 Code Object、局部变量、全局变量、指令指针(instruction pointer)和栈指针(stack pointer),还包含了大量运行时甚至调试所需的数据。
这种传统调用机制暴露出了两个肉眼可见的瓶颈: 1. **C 语言调用栈(Call Stack)的递归膨胀**:每次在 Python 中发起一次函数调用,底层作为 C 程序的 CPython 也会在 C 的调用栈中新增 `_PyFunction_Vectorcall`、`_PyEval_Vector` 以及新的 `_PyEval_EvalFrameDefault` 帧。这不仅带来了显著的 C 语言函数调用时间开销,还面临有限的 C 栈空间被耗尽导致栈溢出(爆栈)的风险。 2. **堆内存碎片化与缓存极不友好**:尽管存在缓存优化机制,但 Python 频繁访问的局部变量均保存在零散分配于堆(Heap)上的 `PyFrameObject` 中。由于内存不连续,无法充分利用现代 CPU 的缓存行,造成严重的性能损失。
```mermaid flowchart TD accTitle: Python 3.10 函数调用链 accDescr: 展示 Python 3.10 下从 CALL_FUNCTION 字节码触发 C 栈递归增长与堆内存分配的过程
subgraph C_Stack ["CPython C 调用栈 (深度随调用增长)"] A["_PyEval_EvalFrameDefault (当前运行)"] B["_PyFunction_Vectorcall"] C["_PyEval_Vector"] D["_PyEval_EvalFrameDefault (递归新帧)"] A --> B --> C --> D end
subgraph Heap_Mem ["堆内存 (Heap)"] F1["PyFrameObject (旧)"] F2["PyFrameObject (新: 存 debug、局部变量等,离散且不缓存友好)"] end
C -.->|"malloc 分配"| F2 ```
- Python 官方基准:Python 3.11 整体提速平均约 25%,斐波那契/阶乘等纯函数调用型计算提速可达约 70%。 - 测试数据:在同一台机器上运行 `fib(24)`,Python 3.10 耗时约 9ms,Python 3.11 耗时降至 5~6ms。 - Python 3.10 函数调用核心流程:`CALL_FUNCTION` 字节码 $\to$ `_PyFunction_Vectorcall` $\to$ `_PyEval_Vector` $\to$ 从 Heap 分配 `PyFrameObject` $\to$ 递归调用 `_PyEval_EvalFrameDefault`。
---
[05:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=300s) Python 3.11 的内联化与轻量级连续栈帧优化
进入 Python 3.11 之后,原本的 `CALL_FUNCTION` 字节码被废弃,各种函数调用被统一收拢到了 `CALL` 字节码中。当目标是一个常规的 Python 函数(`PyFunction`)时,解释器会触发快速通道(Fast Path),从内存布局和调用机制两方面实现了质的飞跃。
首先,Python 3.11 引入了更加轻量级的 `_PyInterpreterFrame` 来替代厚重的 `PyFrameObject`。构建这一全新数据结构有两大关键优势:一是它剔除了大量运行时并不必须的调试(debug)数据,使其体积比原先轻便许多,创建速度大幅增加;二是这些解释器帧不再零散地分配在系统的堆上,而是统一驻留在 Python 专门为其开辟的一块连续栈空间(custom stack)中。因为连续排列在内存里,帧数据能够持久驻留在现代 CPU 的高速缓存(Cache)之中,获得了极佳的缓存友好度。
其次,在 C 语言的调用机制上,快速通道彻底打破了以往的递归调用链。在完成了必要的参数准备和上下文更新后,快速通道代码末尾并没有发起新的 C 函数调用,而是直接执行 `goto start_frame;`,跳转回当前 `_PyEval_EvalFrameDefault` 函数头部的标签位置。码农高天强调道:“在做完必须的计算之后,cpython并没有在c里面再次递归调用这个eval frame default函数,而是通过保存和更新必要的上下文,在同一个函数里面从头开始。” 这意味着原本需要深层递归的 Python 函数调用,在底层的 C 调用栈中完全没有产生新的 C 栈帧,不仅彻底抹平了 C 级调用的开销,还从根源上节约了 C 栈空间。
```mermaid flowchart TD accTitle: Python 3.11 Fast Path 调用机制 accDescr: 展示 Python 3.11 中 CALL 字节码通过连续栈内存和 goto 循环复用同一 C 栈帧的过程
subgraph C_Call ["CPython C 调用栈 (单帧常驻,无 C 级递归)"] direction TB E["_PyEval_EvalFrameDefault"] L["start_frame 标签 (函数头部)"] G["CALL 字节码 -> Fast Path"]
E --> L L --> G G -->|"goto start_frame (重设上下文从头执行)"| L end
subgraph Frame_Stack ["Python 专有连续内存栈 (缓存高度友好)"] direction LR IF1["_PyInterpreterFrame 1"] IF2["_PyInterpreterFrame 2"] IF3["_PyInterpreterFrame 3 (轻量、紧密排列)"] IF1 --- IF2 --- IF3 end
G -.->|"在预留连续空间中新建"| IF3 ```
需要注意的是,上述基于 `goto start_frame` 和轻量级解释器帧的机制专属于 Fast Path(理想优化路径)。如果遇到无法内联优化的特殊调用情况,CPython 依然会退回(fallback)到传统实现来保证兼容性与稳健性。通过这种连续内存排布与复用同一 C 栈循环的精妙设计,Python 3.11 成功解决了困扰已久的函数调用性能开销问题。
- 字节码整合:Python 3.11 取消了 `CALL_FUNCTION`,改为整合统一的 `CALL` 字节码。 - 帧结构革新:以精简的 `_PyInterpreterFrame` 取代庞大的 `PyFrameObject`,剥离非必要 debug 信息,显著缩短实例化耗时。 - 内存缓存友好:解释器帧分配在 Python 自建的连续内存栈上紧密排列,大幅提升 CPU Cache 命中率。 - 零 C 栈开销:Fast Path 最终通过 `goto start_frame` 重置上下文并在同一 `_PyEval_EvalFrameDefault` 函数内循环,实现调用内联化,避免 C 栈溢出与函数调用开销。
## Mentioned Python, CPython, Python 3.10, Python 3.11, 码农高天
## Video description 这期视频聊一下3.11是如何让python整体提速25%的。主要设计在函数调用的部分的优化机制。
[Source](https://www.youtube.com/watch?v=t0m6R4HUOw8)