克里斯布的回答给了你所有你需要知道的,但如果你想知道血淋淋的细节。。。
但首先,从冗长的分析中得出的结论概括如下:
-
对于自由函数,两者之间没有太大区别
cpdef
并用
cdef
+
def
性能方面。生成的c代码几乎相同。
-
对于绑定方法,
cpdef
-在存在继承层次结构的情况下,这种方法可以稍微快一点,但没有什么好激动的。
-
使用
cpdef
-语法有它的优点,因为生成的代码更清晰(至少对我来说)而且更短。
自由功能:
当我们定义一些愚蠢的事情时,比如:
cpdef do_nothing_cp():
pass
发生以下情况:
-
创建了一个快速c函数(在本例中,它有一个神秘的名称
__pyx_f_3foo_do_nothing_cp
因为我的分机被调用
foo
,但实际上您只需查找
f
前缀)。
-
还创建了一个python函数(称为
__pyx_pf_3foo_2do_nothing_cp
-前缀
pf
),它不会复制代码并在途中的某个地方调用fast函数。
-
将创建一个python包装器,称为
__pyx_pw_3foo_3do_nothing_cp
(前缀
pw
)
-
do_nothing_cp
方法定义被发布,这就是python包装器所需要的,这是存储函数的位置,当
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\u nothing\u cp
在我们的示例中,可以找到python包装器
pw公司
-功能。
-
pw公司
-函数通过函数指针调用,并调用
pf公司
-功能(作为C功能)
-
pf公司
-函数调用fast
f
-功能。
如果我们打电话会怎么样
do\u nothing\u cp
在cython模块内?
def call_do_nothing_cp():
do_nothing_cp()
显然,cython在这种情况下不需要python机器来定位函数-它可以直接使用fast
f
-通过c函数调用的函数,绕过
pw公司
和
pf公司
功能。
如果我们包装
cdef公司
a中的函数
def公司
-功能?
cdef _do_nothing():
pass
def do_nothing():
_do_nothing()
Cython执行以下操作:
-
禁食
_do_nothing
-创建函数,对应于
f
-上述功能。
-
一
pf公司
-功能
do_nothing
创建,调用
_什么都不做
在路上的某个地方。
-
python包装器,即。
pw公司
创建函数,该函数包装
pf公司
-功能
-
该功能绑定到
foo.do_nothing
通过指向python包装器的函数指针
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版本的所有调用
do\u nothing\u 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)
但对于推出的
什么都不做
(我必须承认,我有点惊讶,还不明白原因):
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函数调用的开销相比,差别并不大。
类方法:
由于可能的多态性,对于类方法来说,情况稍微复杂一些。让我们从以下内容开始:
cdef class A:
cpdef do_nothing_cp(self):
pass
乍一看,与上述情况没有太大区别:
-
快速,仅限c,
f
-发出函数的前缀版本
-
python(前缀
pf公司
)发出版本,这将调用
f
-功能
-
python包装器(前缀
pw公司
)包装
pf公司
-版本和用于注册。
-
do\u nothing\u 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-pw-call\u do\u nothing。
-
它调用
B-pf-call_do_nothing
,
-
哪个呼叫
B-f-call_do_nothing
,
-
哪个呼叫
A-f-do_nothing_cp
,绕过
pw公司
和
pf公司
-版本。
添加以下类时会发生什么
C
,它将覆盖
do\u nothing\u 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
C
-class being located and called which means,
pw-call\u do\u nothing“的
B
-正在定位和调用类,
-
哪个呼叫
B-pf-call\u do\u nothing
,
-
哪个呼叫
B-f-call\u do\u nothing
,
-
哪个呼叫
A-f-do_nothing
(我们已经知道!),绕过
pw公司
和
pf公司
-版本。
现在是第四节。step,我们需要调度呼叫
A-f-do_nothing()
为了得到正确的
C.do_nothing()
呼叫幸运的是,我们手头有这个调度!
更复杂的是:如果
C
也是
cdef公司
-课程?调度通过
__dict__
不起作用,因为cdef类没有
__迪克特__
?
对于cdef类,多态性的实现类似于C++'s“虚拟表”,因此在
B.call_do_nothing()
这个
f-do_nothing
-函数不是直接调用的,而是通过指针调用的,指针取决于对象的类(可以看到在中设置的那些“虚拟表
__pyx_pymod_exec_XXX
,例如。
__pyx_vtable_3foo_B.__pyx_base
). 因此
__迪克特__
-签派入
A-f-什么都不做()
-对于纯cdef层次结构,不需要函数。
在性能方面,比较
cpdef
具有
cdef公司
+
def公司
我得到:
cpdef def+cdef
A.do_nothing 107ns 108ns
B.call_nothing 109ns 116ns
所以如果有人,
cpdef
稍微快一点。