【问题标题】:What are the differences between a cpdef and a cdef wrapped in a def?cpdef 和包含在 def 中的 cdef 有什么区别?
【发布时间】:2018-07-29 14:20:26
【问题描述】:

在 Cython 文档中有一个 example,他们提供了两种编写 C/Python 混合方法的方法。一个显式的,带有用于快速 C 访问的 cdef 和用于从 Python 访问的包装器 def:

cdef class Rectangle:
    cdef int x0, y0
    cdef int x1, y1
    def __init__(self, int x0, int y0, int x1, int y1):
        self.x0 = x0; self.y0 = y0; self.x1 = x1; self.y1 = y1
    cdef int _area(self):
        cdef int area
        area = (self.x1 - self.x0) * (self.y1 - self.y0)
        if area < 0:
            area = -area
        return area
    def area(self):
        return self._area()

还有一个使用 cpdef:

cdef class Rectangle:
    cdef int x0, y0
    cdef int x1, y1
    def __init__(self, int x0, int y0, int x1, int y1):
        self.x0 = x0; self.y0 = y0; self.x1 = x1; self.y1 = y1
    cpdef int area(self):
        cdef int area
        area = (self.x1 - self.x0) * (self.y1 - self.y0)
        if area < 0:
            area = -area
        return area

我想知道实际上有什么区别。

例如,当从 C/Python 调用时,任一方法是否更快/更慢?

另外,当子类化/覆盖时,cpdef 是否提供了其他方法所缺少的东西?

【问题讨论】:

  • 一方面,cpdef 方法与def 方法不同,在其签名中具有返回类型。从技术角度来看,cpdef 方法在从 Cython 访问时引入的开销更少,因为它们不会创建 Python 调用堆栈框架。
  • @EliKorvigo 对不起,我有点困惑。你的意思是当从 Cython 使用无类型参数访问时?否则你会使用cdef 方法,对吧?
  • 我不太明白,你是如何解释我的信息的。 cpdef 方法与 cdef 方法具有相同的类型签名
  • @EliKorvigo 我的意思是。在您的原始评论中,您似乎将cpdefdef 进行比较。但是当从 Cython 内部调用时,我认为将其与包装的 cdef 进行比较更合适,因为这就是你在 def 场景中包装的 cdef 所调用的内容。 - 除非参数(您传递的实际参数,而不是签名)由于某种原因未键入。所以我在问你是否是这个意思。

标签: python cython


【解决方案1】:

chrisb 的回答为您提供了您需要知道的所有信息,但如果您喜欢血淋淋的细节......

但首先,简而言之,从冗长的分析中得出的结论如下:

  • 对于免费功能,cpdef 和使用cdef+def 在性能方面推出它并没有太大区别。生成的 c 代码几乎相同。

  • 对于绑定方法,cpdef-approach 在存在继承层次结构的情况下可能会稍微快一些,但没什么可太兴奋的。

  • 使用cpdef-syntax 有其优势,因为生成的代码更清晰(至少对我而言)更短。


免费功能:

当我们定义一些愚蠢的东西时:

 cpdef do_nothing_cp():
   pass

会发生以下情况:

  1. 创建了一个快速的 c 函数(在这种情况下,它有一个神秘的名称 __pyx_f_3foo_do_nothing_cp,因为我的扩展名为 foo,但实际上您只需查找 f 前缀)。
  2. 还创建了一个 python 函数(称为 __pyx_pf_3foo_2do_nothing_cp - 前缀 pf),它不会复制代码并在途中某处调用快速函数。
  3. 创建了一个python-wrapper,称为__pyx_pw_3foo_3do_nothing_cp(前缀pw
  4. 发出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() 时,会发生以下情况:

  1. 找到绑定到名称do_nothing_cp 的函数指针,在我们的例子中是python-wrapper pw-function。
  2. pw-function 通过函数指针调用,并调用 pf-function(作为 C 函数)
  3. pf-function 调用快速的f-function。

如果我们在 cython 模块中调用 do_nothing_cp 会发生什么?

def call_do_nothing_cp():
    do_nothing_cp()

显然,在这种情况下,cython 不需要 python 机器来定位函数 - 它可以通过 c 函数调用直接使用快速的 f 函数,绕过 pwpf 函数。

如果我们将cdef 函数包装在def 函数中会发生什么?

cdef _do_nothing():
   pass

def do_nothing():
  _do_nothing()

Cython 执行以下操作:

  1. 创建了一个快速的_do_nothing-函数,对应于上面的f-函数。
  2. do_nothing 创建了一个pf 函数,它在途中某处调用_do_nothing
  3. 一个python-wrapper,即pw函数被创建,它包装了pf函数
  4. 功能通过函数指针绑定到foo.do_nothing,指向python-wrapper pw-function。

如您所见 - 与 cpdef-方法没有太大区别。

cdef 函数只是简单的 c 函数,但 defcpdef 函数是第一类的 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

乍一看,和上面的例子没有太大区别:

  1. 发出一个快速的、仅限 c 的 f-prefix-version 函数
  2. 发出一个python(前缀pf)版本,调用f-function
  3. python 包装器(前缀pw)包装pf-版本并用于注册。
  4. 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() 时会发生什么?

  1. `B-pw-call_do_nothing' 被定位并被调用。
  2. 它调用B-pf-call_do_nothing
  3. 调用B-f-call_do_nothing
  4. 调用A-f-do_nothing_cp,绕过pwpf-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() 会导致:

  1. B-class 的call_do_nothing' of theC-class being located and called which means,pw-call_do_nothing' 被定位和调用,
  2. 调用B-pf-call_do_nothing
  3. 调用B-f-call_do_nothing
  4. 调用A-f-do_nothing(我们已经知道!),绕过pwpf-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。


关于性能,比较 cpdefcdef+def 我得到:

                          cpdef         def+cdef
 A.do_nothing              107ns         108ns 
 B.call_nothing            109ns         116ns

所以差别不是很大,如果有人,cpdef 稍微快一点。

【讨论】:

  • 如果您想添加任何内容以使其更加明确:这一切如何与cpdef 类方法一起使用(例如,使它们可覆盖)?我怀疑这一切都非常相似,但知道哪些部分被替换以及覆盖方法的调用链是什么会很有趣。 (如果你想解决这个问题,完全是你自己的选择......)
  • @DavidW 在 Cython 文档中偶然发现了 this little nugget。在“Cython 和 Pyrex 之间的差异”一章中。说真的,这正是我应该说的——如果我想确保人们不会找到它。
【解决方案2】:

请参阅 docs here - 对于大多数用途而言,它们实际上是相同的,cpdef 的开销更多,但在继承方面表现更好。

指令 cpdef 使该方法的两个版本可用;一 在 Cython 中使用速度快,在 Python 中使用速度较慢。那么:

这不仅仅是为 cdef 方法提供 python 包装器:与 cdef 方法不同,cpdef 方法 完全可以被 Python 中的方法和实例属性覆盖 子类。与 cdef 相比,它增加了一点调用开销 方法。

【讨论】:

  • 如果您有兴趣,我刚刚悬赏了这个问题。
猜你喜欢
  • 2015-04-06
  • 2016-06-14
  • 2017-05-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-05
  • 2016-10-08
相关资源
最近更新 更多