En los documentos de Cython hay un ejemplo donde se presentan dos formas de escribir un método híbrido C / Python. Una explícita con cdef para un acceso rápido a C y una definición de envoltura para acceso desde Python:
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()
Y uno usando 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
Me preguntaba cuáles son las diferencias en términos prácticos.
Por ejemplo, ¿alguno de los métodos es más rápido / lento cuando se llama desde C / Python?
Además, cuando se crea una subclase / anulación, ¿cpdef ofrece algo de lo que carezca el otro método?
La respuesta de chrisb te da todo lo que necesitas saber, pero si eres un juego de detalles sangrientos …
Pero primero, las conclusiones del extenso análisis a continuación:
Para las funciones gratuitas, no hay mucha diferencia entre cpdef
y su despliegue con cdef
+ def
rendimiento. El código c resultante es casi idéntico.
Para los métodos enlazados, cpdef
-approach puede ser un poco más rápido en presencia de jerarquías de herencia, pero no hay nada de qué emocionarse.
Usar cpdef
-syntax tiene sus ventajas, ya que el código resultante es más claro (al menos para mí) y más corto.
Funciones gratuitas:
Cuando definimos algo tonto como:
cpdef do_nothing_cp(): pass
sucede lo siguiente:
__pyx_f_3foo_do_nothing_cp
porque mi extensión se llama foo
, pero en realidad solo tiene que buscar el prefijo f
). __pyx_pf_3foo_2do_nothing_cp
– prefijo pf
), no duplica el código y llama a la función rápida en algún lugar del camino. __pyx_pw_3foo_3do_nothing_cp
(prefijo pw
) do_nothing_cp
definición del método do_nothing_cp
, para eso se necesita la envoltura de python, y este es el lugar donde se almacena a qué función se debe llamar cuando se invoca foo.do_nothing_cp
. Puedes verlo en el c-code producido aquí:
static PyMethodDef __pyx_methods[] = { {"do_nothing_cp", (PyCFunction)__pyx_pw_3foo_3do_nothing_cp, METH_NOARGS, 0}, {0, 0, 0, 0} };
Para una función cdef
, solo ocurre el primer paso, para una función, solo los pasos 2-4.
Ahora cuando foo.do_nothing_cp()
módulo foo
e invocamos foo.do_nothing_cp()
sucede lo siguiente:
do_nothing_cp
, en nuestro caso python-wrapper pw
-function. pw
-function se llama a través de function-pointer, y llama a pf
-function (como funcionalidad C) pf
llama a la función f
rápida. ¿Qué sucede si llamamos a do_nothing_cp
dentro del módulo cython?
def call_do_nothing_cp(): do_nothing_cp()
Claramente, cython no necesita la maquinaria de python para ubicar la función en este caso; puede usar directamente la función f
rápida mediante una llamada de c-function, evitando las funciones pw
y pf
.
¿Qué sucede si cdef
función cdef
en una def
function?
cdef _do_nothing(): pass def do_nothing(): _do_nothing()
Cython hace lo siguiente:
_do_nothing
_do_nothing rápida, correspondiente a la función f
anterior. pf
para do_nothing
, que llama a _do_nothing
algún lugar del camino. pw
que envuelve la función pf
foo.do_nothing
través del puntero de función a python-wrapper pw
-function. Como se puede ver, no hay mucha diferencia con el cpdef
-approach.
Las cdef
cdef son simplemente una función c, pero las funciones def
y cpdef
son funciones python de la primera clase; puedes hacer algo como esto:
foo.do_nothing=foo.do_nothing_cp
En cuanto al rendimiento, no podemos esperar mucha diferencia aquí:
>>> 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)
Si observamos el código de máquina resultante ( objdump -d foo.so
), podemos ver que el comstackdor de C ha incluido todas las llamadas para la versión 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)
pero no para la do_nothing
(debo confesar, estoy un poco sorprendida y todavía no entiendo las razones):
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)
Esto podría explicar por cpdef
versión cpdef
es un poco más rápida, pero de todos modos la diferencia no es nada en comparación con la sobrecarga de una llamada a la función python.
Métodos de clase:
La situación es un poco más complicada para los métodos de clase, debido al posible polymorphism. Vamos a empezar con:
cdef class A: cpdef do_nothing_cp(self): pass
A primera vista, no hay mucha diferencia en el caso anterior:
f
-prefix-de la función pf
), que llama a la función f
pw
) envuelve la versión pf
y se utiliza para el registro. do_nothing_cp
se registra como un método de clase A
través de tp_methods
-pointer del PyTypeObject
. Como se puede ver en el archivo c producido:
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*/ ... };
Claramente, la versión enlazada debe tener el self
parámetro implícito como un argumento adicional, pero hay algo más: la función f
realiza una función-dispatch si no se llama desde la función pf
correspondiente, este despacho tiene el siguiente aspecto (Sigo sólo las partes importantes):
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.
¿Por qué es necesario? Considera la siguiente extensión foo
:
cdef class A: cpdef do_nothing_cp(self): pass cdef class B(A): cpdef call_do_nothing(self): self.do_nothing()
¿Qué sucede cuando llamamos B().call_do_nothing()
?
B-pf-call_do_nothing
, Bf-call_do_nothing
, Af-do_nothing_cp
, sin pasar Af-do_nothing_cp
pw
y pf
. ¿Qué sucede cuando agregamos la siguiente clase C
, que anula la función do_nothing_cp
-function?
import foo def class C(foo.B): def do_nothing_cp(self): print("I do something!")
Ahora llamando a C().call_do_nothing()
lleva a:
call_do_nothing' of the
-class being located and called which means,
C -class being located and called which means,
pw-call_do_nothing’ de la clase B
se localiza y se llama, B-pf-call_do_nothing
, Bf-call_do_nothing
, Af-do_nothing
(como ya sabemos), sin pasar por pw
y pf
-versions. Y ahora, en el paso 4., debemos enviar la llamada en Af-do_nothing()
para obtener la llamada C.do_nothing()
correcta. Por suerte tenemos este despacho en la función a mano!
Para hacerlo más complicado: ¿qué cdef
si la clase C
también fuera una clase cdef
? El envío a través de __dict__
no funcionaría, porque cdef-classes no tienen __dict__
?
Para las clases cdef, el polymorphism se implementa de manera similar a las “tablas virtuales” de C ++, por lo que en B.call_do_nothing()
la función f-do_nothing
no se llama directamente sino a través de un puntero, que depende de la clase del objeto (uno puede ver esas “tablas virtuales” configuradas en __pyx_pymod_exec_XXX
, por ejemplo, __pyx_vtable_3foo_B.__pyx_base
). Por lo tanto, el __dict__
-dispatch en Af-do_nothing()
-function no es necesario en caso de una jerarquía cdef pura.
En cuanto al rendimiento, comparando cpdef
con cdef
+ def
obtengo:
cpdef def+cdef A.do_nothing 107ns 108ns B.call_nothing 109ns 116ns
así que la diferencia no es tan grande, si alguien, cpdef
es un poco más rápido.
Vea los documentos aquí : para la mayoría de los propósitos son prácticamente los mismos, cpdef tiene un poco más de sobrecarga pero juega mejor con la herencia.
La directiva cpdef pone a disposición dos versiones del método; uno rápido para usar desde Cython y otro más lento para usar desde Python. Entonces:
Esto hace un poco más que proporcionar un envoltorio de Python para un método cdef: a diferencia de un método cdef, un método cpdef es completamente reemplazable por métodos y atributos de instancia en las subclases de Python. Añade una pequeña sobrecarga de llamadas en comparación con un método cdef.