为什么__setitem__比cdef-classes的等效“普通”方法快得多?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么__setitem__比cdef-classes的等效“普通”方法快得多?相关的知识,希望对你有一定的参考价值。
看起来,对于Cython的cdef类,使用类特殊方法有时比相同的“通常”方法更快,例如__setitem__
比setitem
快3倍:
%%cython
cdef class CyA:
def __setitem__(self, index, val):
pass
def setitem(self, index, 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, index, val):
pass
def setitem(self, index, 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化的?
通用Python方法调用有相当多的开销 - Python查找相关属性(字典查找),确保属性是可调用对象,并且一旦调用它就处理结果。这种开销也适用于def
类的通用cdef
函数(唯一的区别在于该方法的实现在C中定义)。
但是,可以优化C / Cython类的特殊方法,如下所示:
查找速度
作为一种捷径,Python C API中的PyTypeObject
定义了许多不同的“槽” - 特殊方法的直接函数指针。对于__setitem__
,实际上有两个可用:PyMappingMethods.mp_ass_subscript
对应于一般的“映射”调用,而PySequenceMethods.sq_ass_item
,它允许你直接使用int作为索引器并对应于C API函数PySequence_SetItem
。
对于cdef class
,Cython似乎只生成第一个(通用),因此加速不是直接传递C int
。在生成非cdef
类时,Cython不会填充这些插槽。
这些的优点是(对于C / Cython类)找到__setitem__
function just involves a couple of pointer NULL checks followed by a C function call。这也适用于__len__
,PyTypeObject
也是由__setitem__
的插槽定义的
相反,
- 对于调用uses a default implementation的Python类,它改为
"__setitem__"
,它为字符串cdef
执行字典查找。 - 对于调用非特殊
def
函数的setitem
或Python类,从类/实例字典中查找该属性(速度较慢)
请注意,如果cdef class
常规函数在cpdef
中被定义为PyTypeObject
(并且从Cython调用),那么Cython实现了自己的机制以便快速查找。
呼唤效率
找到属性后必须调用它。如果从__setitem__
检索特殊函数(例如__len__
上的cdef class
和PyObject
),它们只是C函数指针,因此可以直接调用。
对于其他每种情况,必须对从属性查找中检索到的__setitem__
进行评估以查看它是否可调用,然后调用。
退货处理
当从PyTypeObject
调用__len__
作为特殊函数时,返回值是一个int,它只是用作错误标志。不需要引用计数或处理Python对象。
当PyTypeObject
作为特殊函数从Py_ssize_t
调用时,返回类型是setitem
,必须转换为Python对象,然后在不再需要时销毁。
对于普通函数(例如,从Python或Cython类调用的__setitem__
,或Python类中定义的PyObject*
),返回值是%%cython
cdef class CyA:
# special functions
def __setitem__(self, index, val):
pass
def __getitem__(self, index):
pass
,必须适当地引用计数/销毁。
总之,差异实际上与查找和调用函数的快捷方式有关,而不是函数的内容是否是Cython化。
@ DavidW的答案击中了头部的钉子,这里有一些更多的实验和细节证实了他的答案。
无论有多少参数,调用一个快速返回'Noone`的特殊函数:
a=CyA()
%timeit a[0] # 29.8 ns ± 1.9 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%timeit a[0]=3 # 29.3 ns ± 0.942 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
现在
*args
功能的签名是已知的,没有必要构建**kwargs
,%%cython
cdef class CyA:
...
# normal functions:
def fun0(self):
pass
def fun1(self, arg):
pass
def fun2(self, arg1, arg2):
pass
。插槽中的查找速度和它一样快。
调用普通函数的开销取决于参数的数量:
a=CyA()
...
%timeit a.fun0() # 64.1 ns ± 2.49 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%timeit a.fun1(1) # 67.6 ns ± 0.785 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%timeit a.fun2(2,3) # 94.7 ns ± 1.04 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
现在:
self
开销大于从槽中调用方法的开销,但如果有(至少)两个参数(不考虑65ns
)也更大:95ns
vs METH_NOARGS
。
原因是:cython-methods可以是以下类型之一
self
- 只有参数METH_O
self
- 只有METH_VARARGS|METH_KEYWORDS
+一个参数fun2
- 具有任意数量的元素
方法*args
是第三种类型,因此为了被称为Python,必须构造列表%%cython
cdef class CyA:
...
def __len__(self):
return 1 # return 1000 would be slightly slower
def len(self):
return 1
,这会导致额外的开销。
**从特殊方法返回可能会产生比正常方法更多的开销“:
a=CyA()
...
%timeit len(a) # 52.1 ns ± 1.57 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%timeit a.len() # 57.3 ns ± 1.39 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
导致:
__len__
正如@DavidW指出的那样,对于Py_ssize_t
,在每次调用中,必须从返回的1
构造一个“新的”int对象(在len()
的情况下,它是来自池的整数,所以它不是真正构造的 - 但它是如果数字较大)。
这不是len()
的情况:对于这个特殊的实现,Cython初始化一个全局对象,由__len__
返回 - 增加引用计数器的成本并不高(与创建整数相比!)。
因此,len()
和qazxswpoi的运行速度大致相同 - 但是时间用于不同的事情(创建整数与查找开销)。
以上是关于为什么__setitem__比cdef-classes的等效“普通”方法快得多?的主要内容,如果未能解决你的问题,请参考以下文章
\_\_setitem\_\_和\_\_getitem和\_\_delitem__
__setitem__,__getitem,__delitem__
python学习之__getitem__,__setitem__,__delitem__
python - __setitem__/__getitem__/__delitem__类的内置方法