有一个好消息和一个坏消息:其他模块无法进行内联,但您不必支付 Python 函数调用的全部费用。
什么是内联?它由 C 编译器完成:当 C 编译器知道函数的定义时,它可以决定内联它。这有两个好处:
- 您不必支付调用函数的开销
- 它使进一步优化成为可能。
参见示例:
%%cython -a
ctypedef unsigned long long ull
cdef ull doit(ull a):
return a
def calc_sum_fun():
cdef ull res=0
cdef ull i
for i in range(1000000000):#10**9
res+=doit(i)
return res
>>> %timeit calc_sum_fun()
53.4 ns ± 1.4 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
如何在 53 纳秒内完成 10^9 次加法?因为它没有完成:C 编译器内联 cdef doit() 并且能够在编译器期间计算循环的结果。因此,在运行期间,程序简单地返回预先计算的结果。
从那里很明显,C 编译器将无法内联来自另一个模块的函数,因为该定义在另一个 c 文件/翻译单元中对其隐藏。示例见:
#simple.pdx:
ctypedef unsigned long long ull
cdef ull doit(ull a)
#simple.pyx:
cdef ull doit(ull a):
return a
def doit_slow(a):
return a
现在从另一个 cython 模块访问它:
%%cython
cimport simple
ctypedef unsigned long long ull
def calc_sum_fun():
cdef ull res=0
cdef ull i
for i in range(10000000):#10**7
res+=doit(i)
return res
导致以下时间:
>>> %timeit calc_sum_fun()
17.8 ms ± 208 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
因为内联是不可能的,所以函数确实必须执行循环......但是,它比普通的python调用更快,我们可以通过将cdef doit()替换为def doit_slow()来做到这一点:
%%cython
import simple #import, not cimport
ctypedef unsigned long long ull
def calc_sum_fun_slow():
cdef ull res=0
cdef ull i
for i in range(10000000):#10**7
res+=simple.doit_slow(i) #slow
return res
Python 调用慢了大约 50 倍!
>>> %timeit calc_sum_fun_slow()
1.07 s ± 20.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
但是你问的是类方法而不是全局函数。对于类方法,即使在同一个模块中也不可能内联:
%%cython
ctypedef unsigned long long ull
cdef class A:
cdef ull doit(self, ull a):
return a
def calc_sum_class():
cdef ull res=0
cdef ull i
cdef A a=A()
for i in range(10000000):#10**7
res+=a.doit(i)
return res
导致:
>>> %timeit calc_sum_class()
18.2 ms ± 264 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
这与在另一个模块中定义cdef类的情况基本相同。
这种行为的原因是 cdef 类的构建方式。它与 C++ 中的虚拟类有很大不同——类定义类似于名为 __pyx_vtab 的虚拟表:
struct __pyx_obj_12simple_class_A {
PyObject_HEAD
struct __pyx_vtabstruct_12simple_class_A *__pyx_vtab;
};
指向cdef doit() 的指针保存在哪里:
struct __pyx_vtabstruct_12simple_class_A {
__pyx_t_12simple_class_ull (*doit)(struct __pyx_obj_12simple_class_A *, __pyx_t_12simple_class_ull);
};
当我们调用a.doit() 时,我们不是直接调用函数,而是通过这个指针:
((struct __pyx_vtabstruct_12simple_class_A *)__pyx_v_a->__pyx_vtab)->doit(__pyx_v_a, __pyx_v_i);
这解释了为什么 C 编译器不能内联函数 doit()。