Python 3.11重构函数调用机制 消除C级递归提速25%

【python】3.11是如何变快的?我来给你讲一下其中原理! — 码农高天

码农高天

新版本通过连续栈内存替代堆分配帧,并以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)

中文简报

Python 3.11重构函数调用机制 消除C级递归提速25% 新版本通过连续栈内存替代堆分配帧,并以goto循环复用单层解释器函数,彻底削减了深层调用引发的C栈与缓存开销。 视频深入剖析了Python 3.11性能跃升的技术内幕,对比了3.10与3.11在函数调用底层实现上的根本差异。在Python 3.10中,函数调用依赖CALL_FUNCTION字节码,存在C调用栈递归膨胀与堆内存中PyFrameObject离散分配导致的性能瓶颈与缓存不友好问题。而Python 3.11通过引入统一的CALL字节码、轻量级且紧凑排布在连续栈内存中的_PyInterpreterFrame,以及利用goto start_frame避免C级递归的内联优化,极大抹平了调用开销并提升了CPU缓存命中率。 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)

Reading summary

根据码农高天的解析,Python 3.11 取得显著性能提升的核心原因之一在于对函数调用的深度优化,特别是通过轻量级连续栈帧(Lazy Frames)与内联调用(Inline Function Calls)技术,大幅降低了调用开销与 C 栈消耗。 Python 3.11 性能表现与调用瓶颈 (0:00) Python 官方数据显示,3.11 版本整体性能平均提升约 25%,而在纯函数调用的密集场景下(如递归实现的斐波那契数列或阶乘),提速可达 70% 左右。实测表明,在相同设备上运行 fib(24),Python 3.10 耗时约 9 毫秒,而 Python 3.11 缩短至 5 到 6 毫秒之间。 在 Python 3.10 中,函数调用依赖 CALL_FUNCTION 字节码,随后进入 _PyFunction_Vectorcall 与 _PyEval_Vector,并在堆(Heap)上为每次调用动态分配完整的 PyFrameObject。这导致两个主要瓶颈:一是由于每次 Python 调用都会在 C 语言层面递归调用 _PyEval_EvalFrameDefault,引发 C 调用栈急剧膨胀甚至面临栈溢出风险;二是堆上分配的帧对象包含大量调试数据且分布离散,内存不连续导致 CPU 缓存命中率极低。 精简帧结构与连续栈内存优化 (5:00) Python 3.11 将原有的 CALL_FUNCTION 整合为统一的 CALL 字节码,并在常规 Python 函数调用时启用快速通道(Fast Path)。该版本用精简的 _PyInterpreterFrame 替代了以往厚重的 PyFrameObject,直接剔除了大量非必须的调试数据,显著减少了单帧创建的时间与内存开销。 更为关键的是内存布局的改变。Python 3.11 不再将栈帧零散分配在系统堆上,而是统一存放在 Python 自建的连续内存栈中。由于所有解释器帧在内存中紧密相连,数据能够高效驻留在 CPU 高速缓存(Cache)内,彻底解决了此前堆内存碎片化与缓存不友好的问题。 内联执行与消除 C 栈递归 (5:00) 在 C 语言的执行流程上,Python 3.11 的快速通道打破了多层 C 函数相互调用的传统模式。完成参数准备与上下文更新后,解释器不再递归调用 _PyEval_EvalFrameDefault,而是通过 goto start_frame 直接跳转回当前函数头部的标签位置,从头复用同一份执行上下文。 这种设计在底层彻底抹平了 Python 调用所伴随的 C 栈帧增长,既避免了重复进出 C 函数的调度开销,又消除了 C 栈空间耗尽的隐患。当遇到特殊或无法内联的调用场景时,CPython 仍会退回(fallback)至传统机制以保障代码兼容性。 Python 3.11 通过引入轻量连续的 _PyInterpreterFrame 与基于 goto 的同帧内联循环,解决了困扰 CPython 已久的调用性能瓶颈;对于无法进入快速通道的特殊调用,解释器依然保留传统回退路径以兼顾整体稳定性。

精炼阅读摘要

码农高天指出,Python 3.11 性能提升的核心关键在于重构了函数调用机制,通过精简连续的解释器帧与单 C 栈帧循环复用,大幅削减了传统调用中的内存分配与 C 级递归开销。 性能表现与调用瓶颈 (0:00) Python 官方数据显示,Python 3.11 整体性能平均提升约 25%,而在纯函数调用密集的场景(如递归版斐波那契数列和阶乘计算)中,速度提升可高达约 70%。实测运行 fib(24) 时,Python 3.10 耗时约 9 毫秒,Python 3.11 则缩短至 5 到 6 毫秒。官方将此归因于延迟帧求值(Lazy Frames)与内联函数调用(Inline Function Calls)两项关键优化。 旧版架构的双重瓶颈 (0:00) 在 Python 3.10 中,函数调用由 CALL_FUNCTION 字节码经 _PyFunction_Vectorcall 和 _PyEval_Vector 处理,最终递归调用包含主执行循环的 _PyEval_EvalFrameDefault。这一过程伴随两大缺陷:首先,每次 Python 调用均会在 C 语言调用栈上层叠新帧,带来显著的调用耗时,并存在耗尽 C 栈引发溢出的风险;其次,所有执行上下文均封装在庞大的 PyFrameObject 中,其在堆(Heap)上离散分配且携带过多非必要的调试数据,内存不连续导致 CPU 高速缓存命中率低下。 轻量连续的栈帧设计 (5:00) Python 3.11 废弃了 CALL_FUNCTION 并统一归拢为 CALL 字节码。对于常规 Python 函数调用,解释器引入了更精简的 _PyInterpreterFrame 来取代旧有的 PyFrameObject,剥离了运行时非必须的调试数据以加速创建。同时,这些轻量帧不再散落于堆上,而是集中保存在 Python 自建的连续内存栈空间中,使得局部变量等高频数据紧密排列,显著提升了 CPU 缓存利用率。 消除 C 级递归的快速通道 (5:00) 在快速通道(Fast Path)下,Python 3.11 彻底打破了传统的 C 递归链路。在更新必要上下文后,代码通过底层执行 goto start_frame 直接跳转回当前 _PyEval_EvalFrameDefault 函数头部重新开始,使得 Python 函数在同一个 C 栈帧内部完成迭代执行。这种内联机制完全抹平了 C 级调用本身的性能与空间开销;若遇到无法内联的特殊调用,解释器则会回退至传统兼容路径,兼顾了极致性能与系统稳健性。 Python 3.11 通过引入连续内存栈的轻量帧与 goto 机制避免 C 栈递归,有效化解了长久以来的调用开销,但在特殊调用场景下仍需回退传统路径以保证兼容性。

Detailed notes — zh

## Background 该视频由码农高天于 2024 年 5 月 15 日发布在 YouTube 平台上。视频简介指出,本期内容主要探讨 Python 3.11 是如何让解释器整体提速约 25% 的,重点解析了底层针对函数调用链路所做的核心优化机制。 ## TL;DR 视频深入对比了 Python 3.10 与 3.11 在函数调用机制上的底层实现差异。在 3.10 中,每次函数调用都会在堆内存中分配沉重且分散的 `PyFrameObject`,并触发多层 C 语言递归调用,导致严重的 CPU 缓存不友好与 C 栈开销。Python 3.11 则统一采用新的 `CALL` 字节码并开辟快速通道,引入了剥离调试信息的轻量级 `_PyInterpreterFrame`,将其置于连续栈内存中以极大改善缓存局部性;同时利用 `goto` 跳转实现内联调用,彻底避免了 C 栈的额外递归,从而使得递归与高频调用场景获得了最高达 70% 的性能跃升。 ## 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 指令,为纯 Python 函数调用开辟了专用的快速通道(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) 底层通过 C 语言的 goto 语句直接跳转回解释器头部,实现了真正的内联函数调用,彻底避免了 Python 调用导致的 C 语言调用栈层层递归与栈溢出风险。 ## Key takeaways - [00:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=0s) Python 3.11 平均性能提升达 25%,其中核心提速动力来源于“延迟创建帧”与“内联函数调用”等对函数调用链路的重构。 - [00:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=0s) 在计算第 24 项斐波那契数的纯递归测试中,Python 3.11 运行耗时从 3.10 的约 9ms 缩减至 5~6ms,开销主要集中在调用的代码提速尤为显著。 - [00:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=0s) Python 3.10 在执行 CALL_FUNCTION 字节码时,必须依次调用 call_function、_PyObject_Vectorcall、_PyFunction_Vectorcall 和 _PyEval_Vector。 - [00:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=0s) 旧版本中每次 Python 函数调用都会在 C 语言层面产生真实的函数调用,并递归进入 _PyEval_EvalFrameDefault,导致 C 语言调用栈迅速膨胀并存在爆栈风险。 - [00:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=0s) Python 3.10 每次调用都需要在堆内存中动态分配 PyFrameObject,地址零散分布破坏了 CPU 缓存局部性,且该对象包含大量非核心的调试信息。 - [05:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=300s) Python 3.11 废除 CALL_FUNCTION 并将函数调用逻辑归并为统领全局的 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) 3.11 的解释器帧全部预先分配在连续的栈内存空间中,紧凑排布使 CPU Cache Line 能够高概率命中,带来质的性能飞跃。 - [05:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=300s) Fast Path 利用 C 语言 goto _start_frame 跳转执行新函数字节码,在保持单一 C 栈深度的情况下完成内联调用,彻底消除了 C 递归压栈开销。 - [05:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=300s) 遇到不支持快速路径的特殊调用或异常场景时,Python 3.11 仍保留平滑回退(fallback)至传统调用流程的兼容机制。 ## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=0s) Python 3.10 的函数调用开销与底层瓶颈 在 Python 3.11 正式发布时,最吸引开发者眼球的莫过于其性能表现。Python 官方公布的数据表明,3.11 相比之前版本在性能上平均提升了约 25%,而在某些特定场景下提速甚至更为夸张。码农高天以一段非常经典的递归版斐波那契数列计算为例:在 Python 3.10 下运行计算第 24 项斐波那契数,耗时大约在 9 毫秒左右;而切换到 Python 3.11 运行相同的代码,耗时骤减至 5 到 6 毫秒之间,性能提升极其显著。Python 官方文档中将此类提升归结为若干核心优化,其中尤为关键的两项——延迟创建帧(Lazy frames)与内联函数调用(Inline function calls)——均直接作用于 Python 的函数调用链路。官方甚至提到,对于像斐波那契或阶乘这类开销几乎全部集中在函数调用上的纯递归代码,其速度提升大约能够达到 70%。 为了弄清 3.11 到底优化了什么,首先需要剖析 Python 3.10 的底层执行逻辑。在 3.10 中,函数调用主要依赖 `CALL_FUNCTION` 这一字节码指令。进入该字节码的 C 实现后,流程相对简单直白:它会直接调用 `call_function`,其内部核心逻辑是触发 `_PyObject_Vectorcall`。最终,针对纯 Python 函数的调用会落到 `_PyFunction_Vectorcall` 上,该函数紧接着调用 `_PyEval_Vector`。在 `_PyEval_Vector` 内部,系统必须完成两件关键任务:第一,在堆(Heap)内存中分配并建立一个新的 `PyFrameObject`;第二,持有这个新的 frame object 去调用 `PyEval_EvalFrame`,而这会再次递归进入解释器核心的大 `switch-case` 函数——也就是 `_PyEval_EvalFrameDefault`。 从 CPython 解释器的整体架构来抽象,这里存在着相互交织的两大部分:C 语言自身的调用栈(C call stack)以及所使用的堆内存空间。在发起一次新的函数调用前,CPython 自身已经处于一个 `_PyEval_EvalFrameDefault` 函数中,并运行在当前的 `PyFrameObject` 上。当解释器读到 `CALL_FUNCTION` 字节码时,C 调用栈中便顺次压入 `_PyFunction_Vectorcall` 与 `_PyEval_Vector`,为了支持新函数的运行,系统从堆上开辟出一块内存生成全新的 `PyFrameObject`(虽然有对象缓存池,但本质仍在堆上),最后再次压栈调用 `_PyEval_EvalFrameDefault`。码农高天指出,这种设计存在几个显而易见的严重短板: 1. **C 调用栈膨胀**:Python 层面每一次简单的函数调用,都会在 C 语言层面产生多层真实的函数调用开销,不仅耗时,还容易耗尽有限的 C 栈空间导致爆栈。 2. **CPU 缓存极不友好**:运行中最常被高频访问的局部变量(local variables)、指令指针(instruction pointer)和栈指针(stack pointer)都保存在 `PyFrameObject` 里,但这些对象是零散分布在堆内存中的,码农高天调侃道:“如果你不知道什么叫缓存友好回去查书去啊”,这种零散分布直接破坏了现代 CPU 的数据局部性缓存机制。 3. **对象过于沉重**:`PyFrameObject` 内部不仅承载了运行必须的状态,还塞入了大量调试信息(debug data),每次调用都全量构建该对象的开销极其高昂。 ```mermaid flowchart TD accTitle: Python 3.10 函数调用与内存开销 accDescr: 展示 Python 3.10 中字节码触发 C 递归调用并在堆上分散分配 PyFrameObject 的过程 subgraph CStack["C 语言调用栈 (逐层深入)"] A["_PyEval_EvalFrameDefault (外层)"] --> B["CALL_FUNCTION 字节码"] B --> C["_PyFunction_Vectorcall"] C --> D["_PyEval_Vector"] D --> E["_PyEval_EvalFrameDefault (内层递归)"] end subgraph HeapMem["堆内存 (散乱且沉重)"] D -.->|"从堆中申请创建"| F["PyFrameObject<br/>- 包含局部变量/执行指针<br/>- 夹带大量 Debug 数据"] E -.->|"绑定运行"| F end ``` - Python 官方基准:Python 3.11 平均提速约 25%。 - 递归场景对比:斐波那契(第 24 项)从 3.10 的 ~9ms 骤减至 3.11 的 5~6ms;同类高频递归函数提速最高可达 70%。 - 3.10 核心调用链:`CALL_FUNCTION` -> `_PyObject_Vectorcall` -> `_PyFunction_Vectorcall` -> `_PyEval_Vector` -> `_PyEval_EvalFrameDefault`。 - 堆内存负担:旧版每一个 Python 函数调用都需要一个由堆分配的 `PyFrameObject`,该对象尺寸庞大且内存地址分散。 --- ### [05:00](https://www.youtube.com/watch?v=t0m6R4HUOw8&t=300s) Python 3.11 的核心革新:统一 CALL 字节码、轻量级解释器帧与内联调用 进入 Python 3.11 时代,旧有的 `CALL_FUNCTION` 字节码被彻底废弃,官方将所有类型的函数调用逻辑统一合并到了一个全新的 `CALL` 字节码之中。`CALL` 的底层实现相对复杂,但它专门为纯 Python 函数(`PyFunction`)开辟了一条极致优化的“快速通道”(Fast Path)。在这条快速路径上,CPython 从两个维度彻底重构了运行逻辑:引入了更为精炼的解释器帧结构 `_PyInterpreterFrame`,并通过 C 语言内部的直接跳转实现了真正的“内联函数调用”。 首先在数据结构层面,Python 3.11 抛弃了原来每次调用都构建沉重 `PyFrameObject` 的做法,取而代之的是轻量级的 `_PyInterpreterFrame`。码农高天强调,开发者只需要记住它的两个核心优势:第一,它相比过去的 `PyFrameObject` 极其轻量,果断剔除了海量并不在常规执行中使用的调试信息,仅保留运行时所必需的关键上下文,因此创建成本极低;第二,在内存布局上,这些 interpreter frame 不再散落在堆内存各处,而是全部整齐排列在 Python 专门预先分配的连续栈内存(stack)中。这种紧密相邻的内存排布,确保了 CPU 缓存行(Cache line)能够一次性命中所需数据,对于现代处理器的执行效率带来了质的飞跃。 更为精妙的是 C 语言调用层面的彻底解耦。当解释器在 `CALL` 字节码的 Fast Path 中准备妥当所需的参数和局部数据后,它并没有像 3.10 那样去发起新的 C 函数调用来递归执行 `_PyEval_EvalFrameDefault`,而是利用 C 语言的 `goto` 语句直接跳转到当前执行函数的头部标签 `_start_frame`。这意味着,在 Python 层面发生了一次新的函数调用,但在底层 C 语言的调用栈上,其深度根本没有增加。解释器只是保存并切换了当前指针所指向的连续上下文,随即直接在同一个 `_PyEval_EvalFrameDefault` 循环中从头执行新函数的字节码。这种内联调用一举铲除了 C 函数调用本身的开销,也根治了 C 栈空间被轻易撑爆的隐患。 码农高天最后提醒大家,这一套极致优化适用于纯 Python 函数调用的理想路径:“我们这说的是 fast path 就是理想的情况或者说它优化的情况,那还有一些情况呢是不能被这种方式优化的,这种时候呢它就会 fallback 到原来的实现里。”虽然某些非 Python 原生函数或异常场景仍会回退到传统机制,但绝大多数典型工作负载都享受到了轻量帧与内联调用的红利,这也正是 Python 3.11 能够整体大幅提速的核心技术秘诀。 ```mermaid flowchart LR accTitle: Python 3.11 Fast Path 调用机制与内存布局 accDescr: 展示 Python 3.11 使用统一 CALL 字节码、连续帧栈以及 goto 跳转实现内联调用的流程 subgraph StackMem["Python 预分配连续内存栈"] direction TB F1["_PyInterpreterFrame (Frame 1)"] F2["_PyInterpreterFrame (Frame 2 - 紧凑连续/轻量)"] F1 --- F2 end subgraph Execution["单一 C 函数: _PyEval_EvalFrameDefault"] direction TB Start["_start_frame (函数头部标签)"] --> Exec["执行当前 Frame 字节码"] Exec --> CallOp["CALL 字节码 (Fast Path)"] CallOp -.->|"在连续栈上快速创建"| F2 CallOp -->|"goto _start_frame (无 C 递归压栈)"| Start end ``` - 字节码重构:废除 `CALL_FUNCTION`,多合一归并为 `CALL` 指令,并为普通 Python 函数提供专用 Fast Path。 - `_PyInterpreterFrame` 特性: - 数据精简:剥离非核心的 debug 字段,初始化开销大幅削减。 - 内存连续:驻留在专用栈式连续内存中,极大提升 CPU Cache 命中率。 - 控制流内联:Fast Path 结尾采用 `goto _start_frame` 替代 C 函数递归调用,实现零额外 C 栈开销。 - 回退保障:遇到不支持 Fast Path 的动态或特殊调用场景,平滑回退(fallback)至兼容机制。 ## Mentioned Python, Python 3.10, Python 3.11, CPython, 码农高天, PyFrameObject, _PyInterpreterFrame ## Video description 这期视频聊一下3.11是如何让python整体提速25%的。主要设计在函数调用的部分的优化机制。 [Source](https://www.youtube.com/watch?v=t0m6R4HUOw8)

Original transcript

[0:00] 前一阵呢python3.11正式release了这个版本有一个非常吸引人的点在于他对性能的优化Python官方给出的数据是平均上大约提升了25%当然在某些情况下提升会更明显我们来看一个例子吧左边是一个非常简单的费哥纳切的地规石线我们在3.10版本运行一下这段代码可以看到费波纳切24大约消耗是9毫秒左右接下来我们在3.11版本再运行一下同样的程序可以看到这个时间骤减到了5毫秒到6毫秒之间这个提升就可以说是非常之巨大的在Python的官网上给出了Python3.11性能提升的几个核心原因这里面的lazy frames跟inline function cost都是对python函数调用的速度提升也正因如此 几乎全部消耗都在函数调用上的地规版废布纳切才有了如此明显的性能提升那Python的官网实际上提到这一点他说像费波纳切或者阶层这样的函数差不多有一个70%的速度提升那我们今天就简单的带大家了解一下这两个优化到底是什么意思首先是python3.10的实现我们只关注call function这个字节码的实现我们可以看到这个函数很好读它基本上就是调用了这个call function函数而在这个函数里面的核心调用是这个py object vector call那对于一个函数来说它最后会落到这个PiFVectorCode这个函数里在这个函数里面它会调用Pieval Vector在拍EvalVector这个函数里面我们注意他要做两件事首先他需要建立一个新的PiFrameObject如果大家看过我关于frame object视频就会知道在python3.11之前,所有的python代码都是在这个frame object里面运行的这个frame object保存了这个code objectlocal variables global variables 包括instruction pointer 包括stack pointer 就保存了所有这个函数运行时需要用到的数据然后在建立这个frame object的时候它是从hip里拿的一块内存在有了这个frame object之后他再去做一个eval frame而这个EvalFrame函数就会再次递归的调用我们刚才看到的那个巨大的switch所在的函数也就是这个EvalFrame Default我们尝试给大家抽象一下这里面的核心内容这里非常重要的两个部分分别是C Python本身的call stack也就是python作为一个c程序,它在c里面的调用站以及他使用的内存我们看在调用一个新的函数之前这个cpython的调用战里面本身也在一个evil frame default函数里另外他现在一定运行在一个frame object下在他遇到了call function这个字节码之后他就开始准备调用函数然后这个C程序的调用站里面就出现这个PiFactorCode然后再出现PiEvalVector这时候由于我们需要一个新的frame object我们就从heap里面新建了一个frame object当然这里是有一些缓存的优化的我们先不去管它紧接着这个py eval vector函数最终调用了 eval frame default函数使用这个新的frame object那这种调用形式有什么问题呢肉眼可见的有这么几个第一非常明显的当我们在python里调用函数的时候我们c里面的call stack也在不断的增长这里首先C的函数调用肯定是有代价的这是时间上的损耗其次C的调用站的大小也是有限的这样有可能会导致爆站对吧那我们再看到内存这边,我们知道frame object里面有python运行时候access最多的local variables这些数据非常的重要也非常的常用但是由于现在的这些frame object都是零散的在heap里的所以他们是非常不缓存友好的如果你不知道什么叫缓存友好回去查书去啊其次,这个frame object里面,除了有python运行的时候所必须的一些数据之外,它还保存了一些debug数据如果你每一个函数调用都要新建这样的一个frame object那即便有frame object这个东西本身缓存的帮忙它的消耗也是很大的因此,python3.11主要是在这两个方向做了改进首先 [5:00] 这个字节码已经不存在了3.1把各种函数的调用都集中到了靠这一个字节码里靠这个字节码的实现就稍微有一些复杂那我们只关注这个可优化的部分当这个函数是一个正儿八经的py function的时候它就可以走这个fast path在fast path里首先它建立了一个新的叫做py interpreter frame的东西对于这个py interpreter frame我们不讲的太深大家只要记住两点第一它比原来的pyframe object要轻很多它里面包含的数据要更少也就是说建立一个这个新的interpreter frame比原来建立一个frame object要快很多其次这个interpreter frame都是生存在python专门给他们建立的一个stack上的换言之在内存的角度看这些frame都是连在一起的来保证它的缓存友好性同时我们可以看到在这个fast path最后结束的时候它并没有调用一个新的函数而是直接go to了start frame这个start frame label就在这个evil frame default函数的头部也就是说现在我们在做一个python的函数调用的时候我们在c的call stack里面实际上是没有新的调用的这样它就既避免了C在函数调用时候的损耗同时还节省了C的这个stacf的空间我们同样也尝试把这件事抽象一下3.11和3.10版本一个不同的点在于在内存里这些interpreter frame object都是紧密排列的了这样这些frame object就会一直在CPU的缓存里对于现代硬件来说这件事对速度提升是非常有帮助的当我们在eval frame default函数里面发现了一个python函数调用的时候他会先在python准备好的这一块连续的内存里面建立一个interpreter frame object注意建立它是比之前建立那个frame object要快的因为他省略了非常多的debug信息接下来在做完必须的计算之后cpython并没有在c里面再次递规调用这个evil frame default函数而是通过保存和更新必要的上下文在同一个函数里面从头开始当然大家一定要注意我们这说的是fast path就是理想的情况或者说它优化的情况那还有一些情况呢是不能被这种方式优化的这种时候呢它就会fallback到原来的实现里好那我们今天的介绍呢是一个比较基础的high level的介绍在这个过程中呢可能有一些点为了帮助大家理解说的并不是百分之百精确的那希望今天讲的内容呢能让大家大概知道为什么3.11变快了好的 这期视频就到这里喜欢的话记得点下关注别忘了一键三雷哦