对于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类中的setitem(...)
快得多,即使两者都被cythonized了?
答案 0 :(得分:3)
泛型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__
的Python类,它代替uses a default implementation来对字符串"__setitem__"
进行字典查找。
对于cdef
或调用非特殊def
函数的Python类,从类/实例字典中查找该属性(速度较慢)
请注意,如果setitem
常规函数将在cdef class
中定义为cpdef
(并从Cython调用),则Cython会实现自己的机制以进行快速查找。 / p>
已找到必须调用的属性。从PyTypeObject
中检索特殊功能的地方(例如__setitem__
上的__len__
和cdef class
),它们只是C函数指针,因此可以直接调用。>
对于其他每种情况,必须对从属性查找中检索到的PyObject
进行评估,以查看它是否是可调用的,然后再调用。
当从__setitem__
作为特殊函数调用PyTypeObject
时,返回值是一个int,它简单地用作错误标志。无需引用计数或处理Python对象。
当从__len__
作为特殊函数调用PyTypeObject
时,返回类型为Py_ssize_t
,必须将其转换为Python对象,然后在不再需要时销毁它。
对于普通函数(例如,从Python或Cython类调用的setitem
或在Python类中定义的__setitem__
),返回值为PyObject*
,必须对其进行引用计数/已适当销毁。
总而言之,区别实际上与查找和调用函数的快捷方式有关,而不是函数的内容是否被Cythonized
。答案 1 :(得分:0)
@DavidW的答案令人震惊,这里有更多的实验和细节证实了他的答案。
无论有多少个参数,调用返回“ None”的特殊函数都是快速的:
%%cython
cdef class CyA:
# special functions
def __setitem__(self, index, val):
pass
def __getitem__(self, index):
pass
现在
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
原因:cython方法可以是以下类型之一
METH_NOARGS
-仅带有参数self
METH_O
-仅带有self
+一个参数METH_VARARGS|METH_KEYWORDS
-具有任意数量的元素方法fun2
是第三种方法,因此要被调用,Python必须构造列表*args
,这会导致额外的开销。
**从特殊方法返回可能比普通方法有更多开销”:
%%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)
正如@DavidW所指出的,对于__len__
,在每次调用中,都必须根据返回的Py_ssize_t
构造一个“新” int对象(对于1
,它是一个池中的整数,因此它不是真正构造的-但在较大数字的情况下是这样的。
len()
并非如此:对于这种特殊的实现,Cython初始化了一个全局对象,该对象由len()
返回-增加引用计数器的成本不高(与创建一个整数!)。
因此,__len__
和len()
的运行速度几乎相等-但时间花费在不同的事情上(创建整数与查找开销)。