chrisb 的回答为您提供了您需要知道的所有信息,但如果您喜欢血淋淋的细节......
但首先,简而言之,从冗长的分析中得出的结论如下:
对于免费功能,cpdef 和使用cdef+def 在性能方面推出它并没有太大区别。生成的 c 代码几乎相同。
对于绑定方法,cpdef-approach 在存在继承层次结构的情况下可能会稍微快一些,但没什么可太兴奋的。
使用cpdef-syntax 有其优势,因为生成的代码更清晰(至少对我而言)更短。
免费功能:
当我们定义一些愚蠢的东西时:
cpdef do_nothing_cp():
pass
会发生以下情况:
- 创建了一个快速的 c 函数(在这种情况下,它有一个神秘的名称
__pyx_f_3foo_do_nothing_cp,因为我的扩展名为 foo,但实际上您只需查找 f 前缀)。
- 还创建了一个 python 函数(称为
__pyx_pf_3foo_2do_nothing_cp - 前缀 pf),它不会复制代码并在途中某处调用快速函数。
- 创建了一个python-wrapper,称为
__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-function 通过函数指针调用,并调用 pf-function(作为 C 函数)
-
pf-function 调用快速的f-function。
如果我们在 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-函数。
- 为
do_nothing 创建了一个pf 函数,它在途中某处调用_do_nothing。
- 一个python-wrapper,即
pw函数被创建,它包装了pf函数
- 功能通过函数指针绑定到
foo.do_nothing,指向python-wrapper pw-function。
如您所见 - 与 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 版本 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
乍一看,和上面的例子没有太大区别:
- 发出一个快速的、仅限 c 的
f-prefix-version 函数
- 发出一个python(前缀
pf)版本,调用f-function
- python 包装器(前缀
pw)包装pf-版本并用于注册。
-
do_nothing_cp 通过tp_methods-PyTypeObject 的指针注册为A 类的方法。
在生成的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 作为附加参数 - 但还有更多:如果不是从相应的 pf 函数调用,f 函数将执行函数调度,这个调度看起来如下(我只保留重要部分):
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-pw-call_do_nothing' 被定位并被调用。
- 它调用
B-pf-call_do_nothing,
- 调用
B-f-call_do_nothing,
- 调用
A-f-do_nothing_cp,绕过pw和pf-versions。
当我们添加以下类 C 时会发生什么,它会覆盖 do_nothing_cp 函数?
import foo
def class C(foo.B):
def do_nothing_cp(self):
print("I do something!")
现在调用C().call_do_nothing() 会导致:
-
B-class 的call_do_nothing' of theC-class being located and called which means,pw-call_do_nothing' 被定位和调用,
- 调用
B-pf-call_do_nothing,
- 调用
B-f-call_do_nothing,
- 调用
A-f-do_nothing(我们已经知道!),绕过pw和pf-versions。
现在在 4. 步骤中,我们需要在 A-f-do_nothing() 中发送呼叫,以便获得正确的 C.do_nothing() 呼叫!幸运的是,我们手头的函数中有这个调度!
更复杂一点:如果C 类也是cdef 类怎么办?通过__dict__ 的调度不起作用,因为cdef-classes 没有__dict__?
对于 cdef 类,多态性的实现类似于 C++ 的“虚拟表”,因此在 B.call_do_nothing() 中,f-do_nothing 函数不是直接调用而是通过指针调用,这取决于对象的类 (可以看到在__pyx_pymod_exec_XXX 中设置了那些“虚拟表”,例如__pyx_vtable_3foo_B.__pyx_base)。因此,在纯 cdef-hierarchy 的情况下,不需要 A-f-do_nothing()-function 中的 __dict__-dispatch。
关于性能,比较 cpdef 和 cdef+def 我得到:
cpdef def+cdef
A.do_nothing 107ns 108ns
B.call_nothing 109ns 116ns
所以差别不是很大,如果有人,cpdef 稍微快一点。