【问题标题】:Inlining a cdef method from a cdef class from another cython package内联来自另一个 cython 包的 cdef 类的 cdef 方法
【发布时间】:2018-05-09 19:14:09
【问题描述】:

我有一个像这样的类:

cdef class Cls:

    cdef func1(self):
        pass

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

【问题讨论】:

  • 我知道链接的问题有点复杂,但答案是一样的,所以我认为这是一个适当的重复
  • @DavidW 如果这个问题是关于“我怎样才能让它工作?”比这是重复的。如果这个问题是关于“为什么我可以打电话给func1 而不是func2?”或“cython 是否能够在另一个模块中内联 func2?”那么这是一个不同的(甚至是非常有趣的)问题。不过,这个问题可能需要稍微润色一下,所以问题的内容一目了然。
  • 我同意你们俩的观点!我的是重复的,但我无法在谷歌上找到它。我将改为内联问题。

标签: cython


【解决方案1】:

有一个好消息和一个坏消息:其他模块无法进行内联,但您不必支付 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()替换为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()

【讨论】:

    猜你喜欢
    • 2013-09-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-06
    相关资源
    最近更新 更多