python – 为什么__setitem__比cdef-classes的等效“普通”方法快得多?

看起来,对于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化的?

最佳答案
通用Python方法调用有相当多的开销 – Python查找相关属性(字典查找),确保属性是可调用对象,并且一旦调用它就处理结果.此开销也适用于cdef类的通用def函数(唯一的区别是该方法的实现在C中定义).

但是,可以优化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 *,必须对其进行适当的引用计数/销毁.

总之,差异实际上与查找和调用函数的快捷方式有关,而不是函数的内容是否是Cython化.

相关文章

本文从多个角度分析了vi编辑器保存退出命令。我们介绍了保存...
Python中的回车和换行是计算机中文本处理中的两个重要概念,...
SQL Server启动不了错误1067是一种比较常见的故障,主要原因...
信息模块是一种可重复使用的、可编程的、可扩展的、可维护的...
本文从电脑配置、PyCharm版本、Java版本、配置文件以及程序冲...
本文主要从多个角度分析了安装SQL Server 2012时可能出现的错...