在Cython文档中有一个example,它们提供了两种编写C / Python混合方法的方法。一个显式的用于快速C访问的cdef和一个用于从Python访问的包装器def:
cdef class Rectangle:
cdef int x0, y0
cdef int x1, y1
def __init__(self, int x0, int y0, int x1, int y1):
self.x0 = x0; self.y0 = y0; self.x1 = x1; self.y1 = y1
cdef int _area(self):
cdef int area
area = (self.x1 - self.x0) * (self.y1 - self.y0)
if area < 0:
area = -area
return area
def area(self):
return self._area()
一个使用cpdef:
cdef class Rectangle:
cdef int x0, y0
cdef int x1, y1
def __init__(self, int x0, int y0, int x1, int y1):
self.x0 = x0; self.y0 = y0; self.x1 = x1; self.y1 = y1
cpdef int area(self):
cdef int area
area = (self.x1 - self.x0) * (self.y1 - self.y0)
if area < 0:
area = -area
return area
我想知道实际上有什么不同。
例如,从C / Python调用时,哪种方法更快/更慢?
另外,当子类化/重写时,cpdef会提供其他方法缺少的东西吗?
答案 0 :(得分:10)
chrisb的回答为您提供了所有您需要知道的内容,但如果您是血腥细节的游戏......
但首先,从冗长的分析中得到的结论概括地说:
对于免费功能,cpdef
与cdef
+ def
表现之间的差异不大。生成的c代码几乎完全相同。
对于绑定方法,cpdef
- 在存在继承层次结构的情况下,方法可以稍微快一点,但没有什么可以过于兴奋。
使用cpdef
- 语法有其优点,因为生成的代码更清晰(至少对我而言)更短。
免费功能
当我们定义愚蠢的东西时:
cpdef do_nothing_cp():
pass
发生以下情况:
__pyx_f_3foo_do_nothing_cp
,因为我的扩展名称为foo
,但实际上你只需查找f
前缀)。__pyx_pf_3foo_2do_nothing_cp
- 前缀pf
),它不会复制代码并在途中的某处调用快速函数。__pyx_pw_3foo_3do_nothing_cp
(前缀pw
)do_nothing_cp
方法定义,这是python-wrapper所需要的,这是存储调用foo.do_nothing_cp
时应该调用哪个函数的地方。您可以在生成的c代码中看到它:
static PyMethodDef __pyx_methods[] = {
{"do_nothing_cp", (PyCFunction)__pyx_pw_3foo_3do_nothing_cp, METH_NOARGS, 0},
{0, 0, 0, 0}
};
对于cdef
函数,只有第一步发生,对于def
- 函数仅执行步骤2-4。
现在,当我们加载模块foo
并调用foo.do_nothing_cp()
时,会发生以下情况:
do_nothing_cp
的函数指针,在我们的例子中是python-wrapper pw
- function。pw
- 函数通过函数指针调用,并调用pf
- 函数(作为C函数)pf
- 函数调用快速f
- 函数。如果我们在cython模块中调用do_nothing_cp
会怎样?
def call_do_nothing_cp():
do_nothing_cp()
显然,在这种情况下,cython不需要python机器来定位函数 - 它可以通过c函数调用直接使用快速f
- 函数,绕过pw
和pf
函数。
如果我们在cdef
- 函数中包含def
函数会怎样?
cdef _do_nothing():
pass
def do_nothing():
_do_nothing()
Cython执行以下操作:
_do_nothing
- 函数,对应上面的f
- 函数。pf
- do_nothing
的函数,在途中某处调用_do_nothing
。pw
函数,用于包装pf
- 函数foo.do_nothing
函数的函数指针绑定到pw
。正如您所看到的那样 - 与cpdef
方法没什么区别。
cdef
- 函数只是简单的c函数,但def
和cpdef
函数是第一类的python函数 - 你可以这样做:
foo.do_nothing=foo.do_nothing_cp
至于表现,我们不能指望这里有太大差异:
>>> import foo
>>> %timeit foo.do_nothing_cp
51.6 ns ± 0.437 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
>>> %timeit foo.do_nothing
51.8 ns ± 0.369 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
如果我们查看生成的机器代码(objdump -d foo.so
),我们可以看到C编译器已经内联了cpdef-version do_nothing_cp
的所有调用:
0000000000001340 <__pyx_pw_3foo_3do_nothing_cp>:
1340: 48 8b 05 91 1c 20 00 mov 0x201c91(%rip),%rax
1347: 48 83 00 01 addq $0x1,(%rax)
134b: c3 retq
134c: 0f 1f 40 00 nopl 0x0(%rax)
但不适用于推出的do_nothing
(我必须承认,我有点意外,并且不了解原因):
0000000000001380 <__pyx_pw_3foo_1do_nothing>:
1380: 53 push %rbx
1381: 48 8b 1d 50 1c 20 00 mov 0x201c50(%rip),%rbx # 202fd8 <_DYNAMIC+0x208>
1388: 48 8b 13 mov (%rbx),%rdx
138b: 48 85 d2 test %rdx,%rdx
138e: 75 0d jne 139d <__pyx_pw_3foo_1do_nothing+0x1d>
1390: 48 8b 43 08 mov 0x8(%rbx),%rax
1394: 48 89 df mov %rbx,%rdi
1397: ff 50 30 callq *0x30(%rax)
139a: 48 8b 13 mov (%rbx),%rdx
139d: 48 83 c2 01 add $0x1,%rdx
13a1: 48 89 d8 mov %rbx,%rax
13a4: 48 89 13 mov %rdx,(%rbx)
13a7: 5b pop %rbx
13a8: c3 retq
13a9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
这可以解释为什么cpdef
版本稍微快一点,但无论如何,与python-function-call的开销相比,差别无关。
<强>类的方法:强>
由于可能的多态性,类方法的情况稍微复杂一些。让我们开始:
cdef class A:
cpdef do_nothing_cp(self):
pass
乍一看,与上述情况没有太大区别:
f
- 前缀版本pf
)版本,调用f
- 函数pw
)包装pf
- 版本并用于注册。do_nothing_cp
已通过A
- tp_methods
的指针注册为类PyTypeObject
的方法。从生成的c文件中可以看出:
static PyMethodDef __pyx_methods_3foo_A[] = {
{"do_nothing", (PyCFunction)__pyx_pw_3foo_1A_1do_nothing_cp, METH_NOARGS, 0},
...
{0, 0, 0, 0}
};
....
static PyTypeObject __pyx_type_3foo_A = {
...
__pyx_methods_3foo_A, /*tp_methods*/
...
};
显然,绑定版本必须使用隐式参数self
作为附加参数 - 但还有更多内容:f
- 函数执行函数调度,如果不是从相应的调用pf
函数,此调度看起来如下(我只保留重要部分):
static PyObject *__pyx_f_3foo_1A_do_nothing_cp(CYTHON_UNUSED struct __pyx_obj_3foo_A *__pyx_v_self, int __pyx_skip_dispatch) {
if (unlikely(__pyx_skip_dispatch)) ;//__pyx_skip_dispatch=1 if called from pf-version
/* Check if overridden in Python */
else if (look-up if function is overriden in __dict__ of the object)
use the overriden function
}
do the work.
为什么需要它?请考虑以下扩展程序foo
:
cdef class A:
cpdef do_nothing_cp(self):
pass
cdef class B(A):
cpdef call_do_nothing(self):
self.do_nothing()
当我们致电B().call_do_nothing()
时会发生什么?
B-pf-call_do_nothing
,B-f-call_do_nothing
,A-f-do_nothing_cp
,绕过pw
和pf
版本。当我们添加以下类C
时会发生什么,它会覆盖do_nothing_cp
- 函数?
import foo
def class C(foo.B):
def do_nothing_cp(self):
print("I do something!")
现在致电C().call_do_nothing()
会导致:
call_do_nothing' of the
13 C -class being located and called which means,
PW-call_do_nothing&#39; B
- 被定位和调用的类B-pf-call_do_nothing
,B-f-call_do_nothing
,A-f-do_nothing
(我们已经知道!),绕过pw
和pf
- 版本。现在,在第4步中,我们需要在A-f-do_nothing()
中拨打电话,以便获得正确的C.do_nothing()
电话!幸运的是,我们可以在手头的功能中进行调度!
要使其更复杂:如果班级C
也是cdef
班,该怎么办?通过__dict__
发送邮件不起作用,因为cdef-classes没有__dict__
?
对于cdef-classes,多态性实现类似于C ++&#34;虚拟表&#34;,所以在B.call_do_nothing()
f-do_nothing
- 函数不是直接调用但通过指针,这取决于对象的类(可以看到那些&#34;虚拟表&#34;在__pyx_pymod_exec_XXX
中设置,例如__pyx_vtable_3foo_B.__pyx_base
)。因此,在纯cdef层次结构的情况下,不需要__dict__
- A-f-do_nothing()
- 函数中的调度。
至于效果,将cpdef
与cdef
+ def
进行比较我得到:
cpdef def+cdef
A.do_nothing 107ns 108ns
B.call_nothing 109ns 116ns
所以差异并不大,如果有人,cpdef
稍快一点。
答案 1 :(得分:1)
请参阅文档here - 对于大多数用途,它们实际上是相同的,cpdef的略微更多的开销但是继承更好。
指令cpdef提供了两种版本的方法;一 从Cython中快速使用,从Python中使用较慢。然后:
这比为cdef方法提供python包装更多:不像cdef方法,cpdef方法 可以通过Python中的方法和实例属性完全覆盖 子类。与cdef相比,它增加了一点调用开销 方法