为何我扩展的Python类型AttrCache运行速度远慢于原生dict?
我看了你实现的AttrCache包装类型代码,发现几个核心点导致它比原生PyDictObject慢不少,咱们一步步拆解问题,再给出优化方向:
先看你提供的核心代码片段
typedef struct { PyObject_HEAD PyDictObject *m; } AttrCache; static Py_ssize_t cache_length(AttrCache* self) { return self->m->ma_used; } static PyObject * cache_subscript(AttrCache* self, register PyObject*key) { PyMappingMethods *m; m = self->m->ob_type->tp_as_mapping; return m->mp_subscript(self->m, key); }
性能瓶颈分析
1. 重复查找映射方法指针的开销
在cache_subscript里,你每次都要从self->m->ob_type取出tp_as_mapping,再调用它的mp_subscript。而原生字典的tp_as_mapping是固定指向dict_as_mapping的,每次重复查找这个指针会产生不必要的内存访问和间接开销——高频调用下这种小开销会被快速放大。
2. 多了一层函数转发的间接开销
你的cache_subscript相当于一个“中间转发器”:先调用自己的函数,再转发到原生字典的mp_subscript。而原生字典的下标访问直接调用dict_subscript,少了这一层函数调用的额外开销。
3. 额外的结构体解引用步骤
每次访问字典内容时,你都要先解引用self->m才能拿到实际的PyDictObject,这比直接操作PyDictObject多了一次内存寻址的步骤,在频繁操作时会积累可观的耗时。
优化方案
方案1:提前缓存原生字典的映射方法指针
在模块初始化阶段,把PyDict_Type的tp_as_mapping指针缓存起来,不用每次在函数里重复查找:
static PyMappingMethods *dict_mapping = NULL; // 模块初始化函数中添加缓存逻辑 int AttrCache_Init(PyObject *module) { dict_mapping = PyDict_Type.tp_as_mapping; if (!dict_mapping) { return -1; } // 其他初始化逻辑... return 0; } // 优化后的cache_subscript static PyObject * cache_subscript(AttrCache* self, PyObject* key) { return dict_mapping->mp_subscript(self->m, key); }
方案2:直接绑定原生字典的映射方法,跳过转发层
给AttrCache的类型对象直接设置tp_as_mapping,让它的映射方法直接指向原生字典的实现,连转发函数的开销都省掉:
// 直接复用原生字典的映射方法(需确保能访问到dict_subscript等内部函数) static PyMappingMethods attrcache_as_mapping = { (lenfunc)cache_length, // mp_length (binaryfunc)dict_subscript, // 直接指向原生字典的下标访问函数 NULL, // mp_ass_subscript(如果需要支持赋值再实现) }; // AttrCache的类型定义 static PyTypeObject AttrCacheType = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "yourmodule.AttrCache", .tp_basicsize = sizeof(AttrCache), .tp_as_mapping = &attrcache_as_mapping, // 其他类型字段... };
方案3:减少不必要的内存访问
现代编译器会自动优化寄存器变量,不用手动加register关键字。另外,确保你的结构体定义紧凑,避免不必要的内存对齐浪费——比如PyObject_HEAD之后直接紧跟PyDictObject *m,没有冗余字段。
方案4:检查引用计数的额外开销
如果你的代码里有多余的Py_INCREF/Py_DECREF操作(比如对返回值做不必要的引用计数调整),也会拖慢速度。原生dict_subscript返回的对象已经是正确的引用计数,直接返回即可。
额外建议
如果你的AttrCache只是做简单包装,也可以考虑直接继承PyDictObject(需要注意Python可变类型继承的内存布局和初始化逻辑),这样能彻底消除一层间接引用的开销。
内容的提问来源于stack exchange,提问作者Christliu

