代码之家  ›  专栏  ›  技术社区  ›  Paul Panzer

cpdef和封装在def中的cdef之间有什么区别?

  •  10
  • Paul Panzer  · 技术社区  · 8 年前

    在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 回复  |  直到 8 年前
        1
  •  15
  •   ead    8 年前

    克里斯布的回答给了你所有你需要知道的,但如果你想知道血淋淋的细节。。。

    但首先,从冗长的分析中得出的结论概括如下:

    • 对于自由函数,两者之间没有太大区别 cpdef 并用 cdef + def 性能方面。生成的c代码几乎相同。

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

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


    自由功能:

    当我们定义一些愚蠢的事情时,比如:

     cpdef do_nothing_cp():
       pass
    

    发生以下情况:

    1. 创建了一个快速c函数(在本例中,它有一个神秘的名称 __pyx_f_3foo_do_nothing_cp 因为我的分机被调用 foo ,但实际上您只需查找 f 前缀)。
    2. 还创建了一个python函数(称为 __pyx_pf_3foo_2do_nothing_cp -前缀 pf ),它不会复制代码并在途中的某个地方调用fast函数。
    3. 将创建一个python包装器,称为 __pyx_pw_3foo_3do_nothing_cp (前缀 pw )
    4. do_nothing_cp 方法定义被发布,这就是python包装器所需要的,这是存储函数的位置,当 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\u nothing\u cp 在我们的示例中,可以找到python包装器 pw公司 -功能。
    2. pw公司 -函数通过函数指针调用,并调用 pf公司 -功能(作为C功能)
    3. pf公司 -函数调用fast f -功能。

    如果我们打电话会怎么样 do\u nothing\u cp 在cython模块内?

    def call_do_nothing_cp():
        do_nothing_cp()
    

    显然,cython在这种情况下不需要python机器来定位函数-它可以直接使用fast f -通过c函数调用的函数,绕过 pw公司 和 pf公司 功能。

    如果我们包装 cdef公司 a中的函数 def公司 -功能?

    cdef _do_nothing():
       pass
    
    def do_nothing():
      _do_nothing()
    

    Cython执行以下操作:

    1. 禁食 _do_nothing -创建函数,对应于 f -上述功能。
    2. 一 pf公司 -功能 do_nothing 创建,调用 _什么都不做 在路上的某个地方。
    3. python包装器,即。 pw公司 创建函数,该函数包装 pf公司 -功能
    4. 该功能绑定到 foo.do_nothing 通过指向python包装器的函数指针 pw公司 -功能。

    正如您所看到的,与 cpdef -接近。

    这个 cdef公司 -函数只是简单的c函数,但是 def公司 和 cpdef 函数是第一个类的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\u nothing\u 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)
    

    但对于推出的 什么都不做 (我必须承认,我有点惊讶,还不明白原因):

    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函数调用的开销相比,差别并不大。


    类方法:

    由于可能的多态性,对于类方法来说,情况稍微复杂一些。让我们从以下内容开始:

    cdef class A:
       cpdef do_nothing_cp(self):
           pass
    

    乍一看,与上述情况没有太大区别:

    1. 快速,仅限c, f -发出函数的前缀版本
    2. python(前缀 pf公司 )发出版本,这将调用 f -功能
    3. python包装器(前缀 pw公司 )包装 pf公司 -版本和用于注册。
    4. do\u nothing\u cp 注册为类的方法 A 通过 tp_methods -的指针 PyTypeObject .

    从生成的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\u do\u nothing。
    2. 它调用 B-pf-call_do_nothing ,
    3. 哪个呼叫 B-f-call_do_nothing ,
    4. 哪个呼叫 A-f-do_nothing_cp ,绕过 pw公司 和 pf公司 -版本。

    添加以下类时会发生什么 C ,它将覆盖 do\u nothing\u 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 C -class being located and called which means, pw-call\u do\u nothing“的 B -正在定位和调用类,
    2. 哪个呼叫 B-pf-call\u do\u nothing ,
    3. 哪个呼叫 B-f-call\u do\u nothing ,
    4. 哪个呼叫 A-f-do_nothing (我们已经知道!),绕过 pw公司 和 pf公司 -版本。

    现在是第四节。step,我们需要调度呼叫 A-f-do_nothing() 为了得到正确的 C.do_nothing() 呼叫幸运的是,我们手头有这个调度!

    更复杂的是:如果 C 也是 cdef公司 -课程?调度通过 __dict__ 不起作用,因为cdef类没有 __迪克特__ ?

    对于cdef类,多态性的实现类似于C++'s“虚拟表”,因此在 B.call_do_nothing() 这个 f-do_nothing -函数不是直接调用的,而是通过指针调用的,指针取决于对象的类(可以看到在中设置的那些“虚拟表 __pyx_pymod_exec_XXX ,例如。 __pyx_vtable_3foo_B.__pyx_base ). 因此 __迪克特__ -签派入 A-f-什么都不做() -对于纯cdef层次结构,不需要函数。


    在性能方面,比较 cpdef 具有 cdef公司 + def公司 我得到:

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

    所以如果有人, cpdef 稍微快一点。

        2
  •  2
  •   chrisb    8 年前

    参见文档 here -在大多数情况下,它们实际上是相同的,cpdef 轻微地 开销更大,但继承性更好。

    指令cpdef提供了该方法的两个版本;一 Cython的使用速度快,Python的使用速度慢。然后:

    这比为cdef方法提供python包装器稍微多一些:与cdef方法不同,cpdef方法 可由Python中的方法和实例属性完全重写 子类。与cdef相比,它增加了一点调用开销 方法