CPython 3.16 字节码变更解析:DELETE_GLOBAL 被 PUSH_NULL + STORE_GLOBAL 取代
2026/9/10 22:31:52 网站建设 项目流程

CPython 3.16 字节码变更解析:DELETE_GLOBAL 被 PUSH_NULL + STORE_GLOBAL 取代

【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython

导读

本篇文章围绕 CPython 3.16 开发周期内一条核心字节码变更展开:del语句删除全局变量时,原先专用的DELETE_GLOBAL操作码已被PUSH_NULL; STORE_GLOBAL组合彻底取代。该变更并非孤立调整,而是与DELETE_NAME → PUSH_NULL; STORE_NAMEDELETE_ATTR → PUSH_NULL; STORE_ATTR同属一个"删除指令统一化"系列。读完本文,你将理解 NULL 哨兵如何让STORE_GLOBAL一个指令同时承载"赋值"与"删除"两种语义、编译器与解释器两侧的对应改动,以及这一调整对 pyc 文件兼容性和dis反汇编输出的实际影响。

一、变更概览:一条 NEWS 背后的架构调整

变更记录位于仓库的 Misc/NEWS.d/next/Core_and_Builtins/2026-03-23-00-45-44.gh-issue-145857.dB66WV.rst,内容只有一句话:

TheDELETE_GLOBALopcode is now replaced withPUSH_NULL; STORE_GLOBAL.

尽管表述简短,但这是一次实实在在的指令集架构调整。从仓库的字节码魔法数字变更记录 Include/internal/pycore_magic_number.h 可以看到,它属于一个连续提交的系列(均落在 3.16a1 阶段):

Python 3.16a1 3702 (Replace DELETE_NAME with PUSH_NULL; STORE_NAME) Python 3.16a1 3703 (Replace DELETE_GLOBAL with PUSH_NULL; STORE_GLOBAL) Python 3.16a1 3704 (Replace DELETE_ATTR with PUSH_NULL; STORE_ATTR) Python 3.16a1 3705 (Add INTRINSIC_ADD_CONDITIONAL_ANNOTATION)

也就是说,DELETE_NAMEDELETE_GLOBALDELETE_ATTR三个"删除类"指令被逐一合并进对应的STORE_*指令中,通过一个 NULL 哨兵值区分"赋值"与"删除"。这种"用哨兵值合并指令"的手法,是 CPython 精简操作码数量、为自适应特化(specializing)解释器铺路的一贯思路——本仓库 Python/bytecodes.c 中大量_PUSH_NULL_CONDITIONAL之类的微指令也体现了同样的设计哲学。

二、新旧字节码对比:del 全局变量如何编译

旧行为:专用 DELETE_GLOBAL 指令

在变更之前,对于下面的 Python 代码:

x = 1 del x

del x这一行会被编译器翻译成一条专用的DELETE_GLOBAL x指令(xco_names中的索引),由解释器执行"从全局命名空间中移除该名字"的操作。

新行为:PUSH_NULL + STORE_GLOBAL

变更之后,编译器的删除路径不再产生独立指令。在编译器的名字处理代码 Python/codegen.c 中,全局作用域名字的三种上下文被统一处理:

case Store: op = STORE_GLOBAL; break; case Del: ADDOP(c, loc, PUSH_NULL); op = STORE_GLOBAL; break;
  • 普通赋值(Store 上下文):直接生成STORE_GLOBAL
  • 删除(Del 上下文):先压入PUSH_NULL(产生 NULL 哨兵),再生成STORE_GLOBAL

因此del x现在的字节码序列为:

PUSH_NULL STORE_GLOBAL x

执行时,解释器看到栈顶是 NULL,就知道应当执行"删除"而非"存储"。

三、STORE_GLOBAL 的语义扩展:NULL 哨兵机制

STORE_GLOBAL的解释逻辑位于 Python/bytecodes.c,其核心在于对栈顶值进行 NULL 判定后分派两条路径:

inst(STORE_GLOBAL, (v --)) { PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); int err; if (PyStackRef_IsNull(v)) { DEAD(v); err = PyDict_Pop(GLOBALS(), name, NULL); if (err == 0) { err = -1; _PyEval_FormatExcCheckArg(tstate, PyExc_NameError, NAME_ERROR_MSG, name); } } else { err = PyDict_SetItem(GLOBALS(), name, PyStackRef_AsPyObjectBorrow(v)); PyStackRef_CLOSE(v); ... } }

拆解其行为:

  • 栈顶为 NULL(删除模式):调用PyDict_Pop(GLOBALS(), name, NULL)从全局字典中移除该名字。若字典中原本不存在该名字(PyDict_Pop返回 0),则构造并抛出NameError——这与旧的DELETE_GLOBAL语义完全一致:del一个不存在的全局变量应当报错;
  • 栈顶为普通对象(赋值模式):调用PyDict_SetItem将值写入全局字典,即原来的普通STORE_GLOBAL行为。

一个关键实现细节是PyStackRef:在 3.16 的栈引用(stack reference)体系中,NULL 由PyStackRef_IsNull()判定,DEAD(v)负责释放引用计数。这使得"哨兵"与"真实空值"可以安全共存于同一栈槽位,不会与合法的None等对象混淆。

指令编号佐证

在 Include/opcode_ids.h 中可以看到两个指令的真实编号:

#define PUSH_NULL 31 #define STORE_GLOBAL 111

PUSH_NULL并非新引入的指令,它原本就用于方法调用时在栈上预留self位置的 NULL 占位(与CALL系列配合)。本次变更等于为其赋予了第二重职责:作为"删除哨兵"。这种复用避免了新增操作码,保持了指令集规模的稳定。

四、配套变更:魔法数字、文档与生成的调度代码

一次操作码语义变更会牵动解释器的多个生成产物:

  1. pyc 魔法数字递增:Include/internal/pycore_magic_number.h 记录为Python 3.16a1 3703。魔法数字是 pyc 文件头的校验值,任何字节码语义变化都必须递增它,否则旧 pyc 缓存会被误加载。因此升级到该版本后,旧版本编译产生的.pyc文件会被 Python 判定为失效并自动重新编译(这是运行时自动完成的,用户无感)。

  2. dis 文档同步DELETE_GLOBAL已从 Doc/library/dis.rst 的操作码文档中移除(文档现只保留STORE_GLOBAL条目,见 Doc/library/dis.rst,其语义描述为"与STORE_NAME相同,但以全局变量方式存储")。同时,文档构建系统的已删除锚点清单 Doc/tools/removed-ids.txt 中登记了library/dis.html: opcode-DELETE_GLOBAL,确保旧链接不会生成死锚点。

  3. 生成的调度代码同步更新:解释器的主调度循环与各特化入口由Python/bytecodes.c自动生成,仓库中 Python/generated_cases.c.h 的TARGET(STORE_GLOBAL)与 Python/executor_cases.c.h 的_STORE_GLOBAL_r10均已按新语义生成,Python/opcode_targets.h中也已不再包含DELETE_GLOBAL的跳转目标。

五、对开发者的影响与验证方法

影响面

  • 使用dis模块的用户:反汇编del global_var时看到的输出从DELETE_GLOBAL变为PUSH_NULL+STORE_GLOBAL
  • 依赖特定字节码序列的工具:如字节码级优化器、混淆器、静态分析器,需要注意删除操作的表示方式已改变;
  • pyc 兼容性:魔法数字 3703 使旧 pyc 缓存失效,属预期行为。

动手验证

在仓库构建出的 Python 3.16 解释器(或任何包含该变更的构建)中,可以直接观察:

import dis def f(): g = 1 del g dis.dis(f)

输出中del g对应行将以PUSH_NULLSTORE_GLOBAL的形式呈现,而不再是DELETE_GLOBAL。若希望对照旧语义,可查看同一仓库历史版本中 Doc/library/dis.rst 对DELETE_GLOBAL的旧描述(del全局名字并从co_names中解析名字索引)作为参照。

总结

DELETE_GLOBAL → PUSH_NULL; STORE_GLOBAL是 CPython 3.16 指令集精简的一部分:编译器在删除路径注入 NULL 哨兵,解释器在STORE_GLOBAL中依据PyStackRef_IsNull分流"赋值/删除"两种操作,从而在不增加新指令的前提下消除专用删除指令。它与DELETE_NAMEDELETE_ATTR的同类变更共同构成一个完整的架构调整,涉及 Python/codegen.c 的代码生成、Python/bytecodes.c 的解释语义、Include/internal/pycore_magic_number.h 的缓存版本以及 Doc/library/dis.rst 的文档同步。对于关注 CPython 字节码演进、解释器实现或字节码工具链的开发者,这是一次值得追踪的指令集设计样本。

【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询