为什么__setitem__比cdef-classes的等效"普通"方法快得多?

ead*_*ead 5 python performance cython python-3.x

看起来,对于Cython的cdef类,使用类特殊方法有时比相同的"常规"方法__setitem__更快,例如比setitem以下快3倍:

%%cython
cdef class CyA:
    def __setitem__(self, index, val):
        pass
    def setitem(self, index, val):
        pass
Run Code Online (Sandbox Code Playgroud)

现在:

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)
Run Code Online (Sandbox Code Playgroud)

这既不是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)
Run Code Online (Sandbox Code Playgroud)

在Cython中,所有特殊功能都不是这样的:

%%cython
cdef class CyA:
    ...
    def __len__(self):
        return 1
    def len(self):
        return 1
Run Code Online (Sandbox Code Playgroud)

这导致:

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)
Run Code Online (Sandbox Code Playgroud)

即几乎相同的运行时间.

为什么__setitem__(...)setitem(...)cdef级更快,即使两者都是cython化的?

Dav*_*idW 4

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

但是,可以优化 C/Cython 类上的特殊方法,如下所示:

查找速度

作为一种快捷方式, PyTypeObjectPython C API 中定义了许多不同的“槽”——特殊方法的直接函数指针。因为__setitem__实际上有两个可用:PyMappingMethods.mp_ass_subscript对应于通用“映射”调用,以及PySequenceMethods.sq_ass_item,它允许您直接使用 int 作为索引器并对应于 C API 函数PySequence_SetItem

对于cdef class,Cython 似乎只生成第一个(通用),因此加速并不是int直接传递 C 。Cython 在生成非类时不会填充这些槽cdef

这些的优点是(对于 C/Cython 类)查找函数__setitem__只涉及几个指针 NULL 检查,然后是 C 函数调用。这也适用于__len__也由插槽定义的PyTypeObject

相比之下,

  • 对于 Python 类调用__setitem__,它使用默认实现来查找字符串的字典"__setitem__"

  • cdef对于调用非特殊函数的a 类或 Python 类def,从类/实例字典中查找属性(速度较慢)

请注意,如果要在assetitem中定义常规函数(并从 Cython 调用),则 Cython 会实现自己的快速查找机制。cdef classcpdef

通话效率

找到属性后,必须调用它。PyTypeObject在从(例如__setitem____len__)检索特殊函数的地方cdef class,它们只是 C 函数指针,因此可以直接调用。

对于所有其他情况,PyObject必须评估从属性查找中检索到的内容以查看它是否是可调用的,然后进行调用。

退货处理

当作为特殊函数__setitem__调用时,PyTypeObject返回值是一个 int,它仅用作错误标志。不需要引用计数或处理 Python 对象。

__len__从 a 作为特殊函数调用时PyTypeObject,返回类型为 a Py_ssize_t,必须将其转换为 Python 对象,然后在不再需要时将其销毁。

对于普通函数(例如setitem,从 Python 或 Cython 类调用,或__setitem__在 Python 类中定义),返回值是 a PyObject*,必须对其进行适当的引用计数/销毁。


总之,区别实际上在于查找和调用函数的快捷方式,而不是函数的内容是否是 Cythonized。