首页 > 解决方案 > 内联来自另一个 cython 包的 cdef 类的 cdef 方法

问题描述

我有一个像这样的类:

cdef class Cls:

    cdef func1(self):
        pass

如果我在另一个库中使用这个类,我可以内联 func1 这是一个类方法吗?或者我应该找到解决方法(例如,通过创建一个将 Cls 指针作为参数的函数?

标签: cython

解决方案


有好消息也有好消息:其他模块无法进行内联,但您不必支付 Python 函数调用的全部费用。

什么是内联?它由 C 编译器完成:当 C 编译器知道函数的定义时,它可以决定内联它。这有两个优点:

  1. 您不必支付调用函数的开销
  2. 它使进一步的优化成为可能。

参见例如:

%%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()through来做到这一点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()


推荐阅读