资讯动态

CPython frozenset 构造性能优化:避免复制,让 frozenset(frozenset) 直接复用原对象

发布时间:2026/9/10 7:19:24 来源:尧图企业网站定制
CPython frozenset 构造性能优化避免复制让 frozenset(frozenset) 直接复用原对象【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本篇技术指南围绕 CPython 的一条核心运行时改进展开frozenset对象在构造阶段避免不必要的元素复制。该改动落在 CPython 内置类型层Core and Builtins读者读完后将理解frozenset的完整构造链路参数校验 → vectorcall → 幂等复用 → 哈希表填充 → GC 跟踪策略并能基于 Objects/setobject.c 中的源码证据判断什么场景下frozenset()是零拷贝的、什么场景下仍会发生全量插入。一、这次优化改了什么对应的变更记录位于 Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-17-16-51.gh-issue-150027.sJgLvd.rst全文只有两行Improve performance offrozensetobjects by avoiding copies during construction.提升frozenset对象性能避免在构造期间产生复制。一句话概括当frozenset()的参数本身就是一个精确的frozenset实例时CPython 不再创建新对象并逐项复制元素而是直接返回原对象的引用。这在 Objects/setobject.c 的make_new_frozenset中实现static PyObject * make_new_frozenset(PyTypeObject *type, PyObject *iterable) { if (type ! PyFrozenSet_Type) { return make_new_set(type, iterable); } if (iterable ! NULL PyFrozenSet_CheckExact(iterable)) { /* frozenset(f) is idempotent */ return Py_NewRef(iterable); } PyObject *obj make_new_set(type, iterable); if (obj ! NULL) { _PyFrozenSet_MaybeUntrack(obj); } return obj; }见 Objects/setobject.c。三个关键细节只对精确类型生效PyFrozenSet_CheckExact(iterable)要求参数必须是frozenset本身而不是它的子类。若参数是frozenset子类实例走的是通用构造路径保证子类的__init__/初始化语义不被短路。Py_NewRef而非返回原指针直接Py_INCREF原对象并返回把引用计数交给调用方符合 CPython 对象所有权约定——调用方拿到返回值后负责释放原对象的存活期因此自然延长。幂等性idempotent是安全前提frozenset(f)的结果在语义上与原对象完全一致——相同的元素、相同的类型且frozenset不可变复用不会引入任何可观察的行为差异。这正是这条优化可以成立的理论依据。二、完整的 frozenset 构造链路从 Python 层的frozenset(x)调用到对象返回实际调用链如下参数入口两种形态经典tp_new路径frozenset_newObjects/setobject.c。它先拒绝关键字参数_PyArg_NoKeywords(frozenset, kwds)报错文本固定为frozenset再用PyArg_UnpackTuple解出 0 到 1 个位置参数。更快的 vectorcall 路径frozenset_vectorcallObjects/setobject.c由类型对象的.tp_vectorcall槽注册见 Objects/setobject.c 附近的PyFrozenSet_Type定义。它跳过tp_new的元对象分发直接校验位置参数个数后进入make_new_frozenset。幂等短路即上文第一节的PyFrozenSet_CheckExact分支。通用构造路径make_new_frozenset落到make_new_set(type, iterable)后者由make_new_set_untrackedObjects/setobject.c完成实体分配so (PySetObject *)_PyType_AllocNoTrack(type, 0); ... so-fill 0; so-used 0; so-mask PySet_MINSIZE - 1; so-table so-smalltable; // 先用对象内嵌的小表 so-hash -1; so-finger 0; so-weakreflist NULL; if (iterable ! NULL) { if (set_update_local(so, iterable)) { ... } }注意注释里写明的设计约束Objects/setobject.cBuild a set/frozenset left GC-untracked; the caller must_PyObject_GC_TRACK()it once fully built, so a half-built set is never exposed during filling.——对象在填充完成前不进入 GC 跟踪避免其他线程看到只填了一半的 set填充成功后才由make_new_set调用_PyObject_GC_TRACK。GC 卸载优化对象建成后make_new_frozenset还会调用_PyFrozenSet_MaybeUntrackObjects/setobject.c。该函数来自 gh-140232 的相关改进遍历 frozenset 的全部元素若没有任何元素是被 GC 跟踪的如全部是 int、str 等不可跟踪对象就把整个 frozenset 从 GC 跟踪列表中移除减少后续 GC 周期的扫描开销。子类实例不参与卸载因为子类可能引入引用环。这条链路说明本次优化是在最坏情况路径之前加了一道最廉价的短路判断——先做类型精确检查O(1)命中即一次Py_INCREF返回未命中才付出分配 逐元素哈希插入 哈希表扩容的成本。三、复制路径到底贵在哪set_update_local 的分发逻辑理解短路收益需要看清被短路掉的那条路径有多重。填充入口是set_update_localObjects/setobject.c它对源对象做了四档分发源对象类型走的路径说明set/frozenset任意集合set_merge_lock_held直接遍历源集合的哈希表逐个set_add_entry可批量预扩容精确dictset_update_dict_lock_held借_PyDict_Next迭代键值对见 Objects/setobject.c可预知PyDict_GET_SIZE并据此一次性扩容Objects/setobject.cfrozendict内部类型同上无需锁frozendict 不可变直接迭代其他可迭代对象set_update_iterable_lock_heldPyObject_GetIter 逐项PyIter_Nextset_add_key最慢对集合源走的是set_merge_lock_held需要Py_BEGIN_CRITICAL_SECTION获取源集合临界区、逐项复制键并处理哈希表扩容set.update这类对已公开对象的更新走set_update_internalObjects/setobject.c还多了一层对so与other的双重临界区保护Py_BEGIN_CRITICAL_SECTION2并对自更新so is other做了直接返回的短路。也就是说优化前frozenset(f)要经历分配新对象 → 复制内嵌表或扩容 → 逐项set_add_entry含哈希查找、冲突处理→ 全程持有源集合临界区 → GC 跟踪。优化后只剩一次引用计数递增。对把 frozenset 当缓存键/常量反复包装的场景例如每层递归都frozenset(prev_keys)这是数量级的差异。四、set为何没有同样待遇对照代码可以明确这次优化的边界set_newObjects/setobject.c只做make_new_set(type, NULL)set(x)若传入集合会完整复制元素——这是刻意的set可变复用同一对象会让两个独立的 set 共享同一份可改数据直接破坏语义。而frozenset不可变 值语义frozenset(f) is f在精确类型下成为可能注意是可能返回的是Py_NewRef引用同一对象因此is恒成立但 frozenset 子类、dict 源、list 源等仍会新建对象。五、如何验证这一行为在任意使用当前仓库构建的 CPython 中可直接观察f frozenset({1, 2, 3}) f2 frozenset(f) print(f2 is f) # True —— 精确类型下直接复用原对象 print(frozenset(frozenset()) is frozenset()) # False —— 两次独立构造 class Sub(frozenset): pass s Sub([1, 2]) print(frozenset(s) is s) # False —— 子类不走幂等短路仍会新建预期结果精确frozenset的二次包装与源对象同一身份子类实例则仍触发完整构造。相关测试入口在 Lib/test/test_set.py其中包含对 frozenset 构造、比较、哈希等行为的成组覆盖回归验证时可优先运行该文件。六、对使用方的实际含义写代码无需改变该优化完全透明frozenset(x)的调用方式、异常行为均不变只是参数恰好是 frozenset这一常见路径变快。性能敏感场景受益以 frozenset 做字典键的嵌套结构如递归解析、DAG 去重、类型参数组合缓存中频繁的frozenset(existing)包装调用现在接近零成本。不要依赖is做逻辑判断虽然精确类型下frozenset(f) is f成立但这属于实现细节层面的幂等性保证仅用于性能不应把它当作 API 契约来写业务逻辑类型上仍是返回一个引用计数递增后的 frozenset。free-threading 环境同样适用填充路径全程使用临界区critical section保护源集合Objects/setobject.c而幂等短路路径不持有任何锁在无 GIL 构建下收益更为直接。七、小结这条两行的 NEWS 背后是 CPython 内置类型层一次典型的识别并消除冗余拷贝优化在make_new_frozenset中利用frozenset的不可变性与精确类型检查将frozenset(f)从分配 全量元素复制 GC 登记降级为一次Py_NewRef。配合既有的 GC 卸载机制_PyFrozenSet_MaybeUntrack与 vectorcall 直达入口frozenset 构造在小集合高频调用的场景下开销被压到最低。阅读 Objects/setobject.c 中make_new_frozenset、set_update_local、set_merge_lock_held三个函数的对照关系是理解 CPython 集合构造快慢分层的最佳切口。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价