包含在def中的cpdef和cdef之间有什么区别?

时间:2018-02-19 10:57:50

标签: python cython

在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会提供其他方法缺少的东西吗?

2 个答案:

答案 0 :(得分:10)

chrisb的回答为您提供了所有您需要知道的内容,但如果您是血腥细节的游戏......

但首先,从冗长的分析中得到的结论概括地说:

  • 对于免费功能,cpdefcdef + def表现之间的差异不大。生成的c代码几乎完全相同。

  • 对于绑定方法,cpdef - 在存在继承层次结构的情况下,方法可以稍微快一点,但没有什么可以过于兴奋。

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

免费功能

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

 cpdef do_nothing_cp():
   pass

发生以下情况:

  1. 创建了一个快速c函数(在这种情况下,它有一个神秘的名称__pyx_f_3foo_do_nothing_cp,因为我的扩展名称为foo,但实际上你只需查找f前缀)。
  2. 还创建了一个python-function(称为__pyx_pf_3foo_2do_nothing_cp - 前缀pf),它不会复制代码并在途中的某处调用快速函数。
  3. 创建了一个python-wrapper,名为__pyx_pw_3foo_3do_nothing_cp(前缀pw
  4. 发出
  5. do_nothing_cp方法定义,这是python-wrapper所需要的,这是存储调用foo.do_nothing_cp时应该调用哪个函数的地方。
  6. 您可以在生成的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 - 函数通过函数指针调用,并调用pf - 函数(作为C函数)
    3. pf - 函数调用快速f - 函数。
    4. 如果我们在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. 创建pf - do_nothing的函数,在途中某处调用_do_nothing
      3. 创建了一个python-wrapper,即pw函数,用于包装pf - 函数
      4. 函数通过指向python-wrapper foo.do_nothing函数的函数指针绑定到pw
      5. 正如您所看到的那样 - 与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-version 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 - 前缀版本
        2. 发出一个python(前缀pf)版本,调用f - 函数
        3. python包装器(前缀pw)包装pf - 版本并用于注册。
        4. do_nothing_cp已通过A - tp_methods的指针注册为类PyTypeObject的方法。
        5. 从生成的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()时会发生什么?

          1. `B-PW-call_do_nothing&#39;找到并打电话。
          2. 它会调用B-pf-call_do_nothing
          3. 调用B-f-call_do_nothing
          4. 调用A-f-do_nothing_cp,绕过pwpf版本。
          5. 当我们添加以下类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. call_do_nothing' of the 13 C -class being located and called which means, PW-call_do_nothing&#39; B - 被定位和调用的类
            2. 调用B-pf-call_do_nothing
            3. 调用B-f-call_do_nothing
            4. 调用A-f-do_nothing(我们已经知道!),绕过pwpf - 版本。
            5. 现在,在第4步中,我们需要在A-f-do_nothing()中拨打电话,以便获得正确的C.do_nothing()电话!幸运的是,我们可以在手头的功能中进行调度!

              要使其更复杂:如果班级C也是cdef班,该怎么办?通过__dict__发送邮件不起作用,因为cdef-classes没有__dict__

              对于cdef-classes,多态性实现类似于C ++&#34;虚拟表&#34;,所以在B.call_do_nothing() f-do_nothing - 函数不是直接调用但通过指针,这取决于对象的类(可以看到那些&#34;虚拟表&#34;在__pyx_pymod_exec_XXX中设置,例如__pyx_vtable_3foo_B.__pyx_base)。因此,在纯cdef层次结构的情况下,不需要__dict__ - A-f-do_nothing() - 函数中的调度。

              至于效果,将cpdefcdef + def进行比较我得到:

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

              所以差异并不大,如果有人,cpdef稍快一点。

答案 1 :(得分:1)

请参阅文档here - 对于大多数用途,它们实际上是相同的,cpdef的略微更多的开销但是继承更好。

  

指令cpdef提供了两种版本的方法;一   从Cython中快速使用,从Python中使用较慢。然后:

     

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