看起来,对于Cython的cdef类,使用类特殊方法有时比相同的“常规”方法更快,例如__setitem__比setitem快3倍:
%%cython
cdef class CyA:
def __setitem__(self,index,val):
pass
def setitem(self,val):
pass
现在:
cy_a=CyA()
%timeit cy_a[0]=3 # 32.4 ns ± 0.195 ns per loop (mean ± std. dev. of 7 runs,10000000 loops each)
%timeit cy_a.setitem(0,3) # 97.5 ns ± 0.389 ns per loop (mean ± std. dev. of 7 runs,10000000 loops each)
这既不是Python的“正常”行为,特殊功能甚至更慢(并且比Cython等效的速度慢):
class PyA:
def __setitem__(self,val):
pass
py_a=PyA()
%timeit py_a[0]=3 # 198 ns ± 2.51 ns per loop (mean ± std. dev. of 7 runs,1000000 loops each)
%timeit py_a.setitem(0,3) # 123 ns ± 0.619 ns per loop (mean ± std. dev. of 7 runs,10000000 loops each)
在Cython中,所有特殊功能都不是这样的:
%%cython
cdef class CyA:
...
def __len__(self):
return 1
def len(self):
return 1
这导致:
cy_a=CyA()
%timeit len(cy_a) # 59.6 ns ± 0.233 ns per loop (mean ± std. dev. of 7 runs,10000000 loops each)
%timeit cy_a.len() # 66.5 ns ± 0.326 ns per loop (mean ± std. dev. of 7 runs,10000000 loops each)
即几乎相同的运行时间.
为什么__setitem __(…)比cdef-class中的setitem(…)快得多,即使两者都是cython化的?
但是,可以优化C/C++ython类的特殊方法,如下所示:
查找速度
作为捷径,
Python C API中的PyTypeObject定义了许多不同的“槽” – 特殊方法的直接函数指针.对于__setitem__,实际上有两个可用:PyMappingMethods.mp_ass_subscript
对应于通用的“映射”调用,而PySequenceMethods.sq_ass_item
,它允许您直接使用int作为索引器并对应于C API函数PySequence_SetItem
.
对于cdef类,Cython似乎只生成第一个(通用)类,因此加速不是直接传递C int.生成非cdef类时,Cython不会填充这些插槽.
这些的优点是(对于C/C++ython类)找到__setitem__
function just involves a couple of pointer NULL checks followed by a C function call.这也适用于__len__,它也由PyTypeObject中的槽定义
相反,
>对于调用__setitem__的Python类,它改为uses a default implementation,它对字符串“__setitem__”进行字典查找.
>对于调用非特殊def函数的cdef或Python类,从类/实例字典中查找该属性(速度较慢)
请注意,如果setitem常规函数要在cdef类中定义为cpdef(并从Cython中调用),那么Cython会实现自己的机制以便快速查找.
呼唤效率
找到属性后必须调用它.在从PyTypeObject中检索特殊函数的地方(例如cdef类中的__setitem__和__len__),它们只是C函数指针,因此可以直接调用.
对于每个其他情况,必须对从属性查找中检索的PyObject进行求值,以查看它是否可调用,然后调用.
退货处理
当从PyTypeObject作为特殊函数调用__setitem__时,返回值是一个int,它只是用作错误标志.不需要引用计数或处理Python对象.
当从PyTypeObject作为特殊函数调用__len__时,返回类型是Py_ssize_t,必须将其转换为Python对象,然后在不再需要时销毁.
对于普通函数(例如,从Python或Cython类调用的setitem,或Python类中定义的__setitem__),返回值是PyObject *,必须对其进行适当的引用计数/销毁.