python - ¿Cuál es la forma recomendada de asignar memoria para una vista de memoria tipada?
memory memory-management (2)
La documentación de Cython sobre las vistas de memoria tipadas enumera tres formas de asignar a una vista de memoria tipada:
- desde un puntero C en bruto,
- de un
np.ndarray
y - desde un
cython.view.array
.
Supongamos que no tengo datos pasados a mi función de cython desde afuera, sino que quiero asignar memoria y devolverla como np.ndarray
, ¿cuál de esas opciones elijo? Supongamos también que el tamaño de ese búfer no es una constante de tiempo de compilación, es decir, no puedo asignar en la pila, pero necesitaría malloc
para la opción 1.
Las 3 opciones, por lo tanto, parecen algo así:
from libc.stdlib cimport malloc, free
cimport numpy as np
from cython cimport view
np.import_array()
def memview_malloc(int N):
cdef int * m = <int *>malloc(N * sizeof(int))
cdef int[::1] b = <int[:N]>m
free(<void *>m)
def memview_ndarray(int N):
cdef int[::1] b = np.empty(N, dtype=np.int32)
def memview_cyarray(int N):
cdef int[::1] b = view.array(shape=(N,), itemsize=sizeof(int), format="i")
Lo que me sorprende es que en los tres casos, Cython genera bastante código para la asignación de memoria, en particular una llamada a __Pyx_PyObject_to_MemoryviewSlice_dc_int
. Esto sugiere (y podría estar equivocado aquí, mi visión del funcionamiento interno de Cython es muy limitada) que primero crea un objeto de Python y luego lo "proyecta" en una vista de memoria, lo que parece una sobrecarga innecesaria.
Un punto de referencia simple no revela revelar mucha diferencia entre los tres métodos, siendo 2. el más rápido por un margen pequeño.
¿Cuál de los tres métodos se recomienda? ¿O hay una opción diferente y mejor?
Pregunta de seguimiento: quiero finalmente devolver el resultado como np.ndarray
, después de haber trabajado con esa vista de memoria en la función. ¿Es la vista de memoria tipada la mejor opción o preferiría simplemente usar la anterior interfaz de búfer como la siguiente para crear una ndarray
en primer lugar?
cdef np.ndarray[DTYPE_t, ndim=1] b = np.empty(N, dtype=np.int32)
Como continuación de la respuesta de Veedrac: tenga en cuenta que el uso de la memoryview
compatible con cpython.array
con python 2.7 parece dar lugar a pérdidas de memoria en la actualidad. Esto parece ser un problema de larga data, ya que se menciona en la lista de correo de cython-users here en una publicación de noviembre de 2012. Ejecutar scrip de referencia de Veedrac con Cython versión 0.22 con Python 2.7.6 y Python 2.7.9 conduce a una se cpython.array
una gran pérdida de memoria al inicializar un cpython.array
utilizando un buffer
o memoryview
interfaz de memoryview
. No se producen pérdidas de memoria al ejecutar el script con Python 3.4. He archivado un informe de error sobre esto en la lista de correo de los desarrolladores de Cython.
Mire here por una respuesta.
La idea básica es que quieras cpython.array.array
y cpython.array.clone
( no cython.array.*
):
from cpython.array cimport array, clone
# This type is what you want and can be cast to things of
# the "double[:]" syntax, so no problems there
cdef array[double] armv, templatemv
templatemv = array(''d'')
# This is fast
armv = clone(templatemv, L, False)
EDITAR
Resulta que los puntos de referencia en ese hilo eran basura. Aquí está mi set, con mis tiempos:
# cython: language_level=3
# cython: boundscheck=False
# cython: wraparound=False
import time
import sys
from cpython.array cimport array, clone
from cython.view cimport array as cvarray
from libc.stdlib cimport malloc, free
import numpy as numpy
cimport numpy as numpy
cdef int loops
def timefunc(name):
def timedecorator(f):
cdef int L, i
print("Running", name)
for L in [1, 10, 100, 1000, 10000, 100000, 1000000]:
start = time.clock()
f(L)
end = time.clock()
print(format((end-start) / loops * 1e6, "2f"), end=" ")
sys.stdout.flush()
print("μs")
return timedecorator
print()
print("INITIALISATIONS")
loops = 100000
@timefunc("cpython.array buffer")
def _(int L):
cdef int i
cdef array[double] arr, template = array(''d'')
for i in range(loops):
arr = clone(template, L, False)
# Prevents dead code elimination
str(arr[0])
@timefunc("cpython.array memoryview")
def _(int L):
cdef int i
cdef double[::1] arr
cdef array template = array(''d'')
for i in range(loops):
arr = clone(template, L, False)
# Prevents dead code elimination
str(arr[0])
@timefunc("cpython.array raw C type")
def _(int L):
cdef int i
cdef array arr, template = array(''d'')
for i in range(loops):
arr = clone(template, L, False)
# Prevents dead code elimination
str(arr[0])
@timefunc("numpy.empty_like memoryview")
def _(int L):
cdef int i
cdef double[::1] arr
template = numpy.empty((L,), dtype=''double'')
for i in range(loops):
arr = numpy.empty_like(template)
# Prevents dead code elimination
str(arr[0])
@timefunc("malloc")
def _(int L):
cdef int i
cdef double* arrptr
for i in range(loops):
arrptr = <double*> malloc(sizeof(double) * L)
free(arrptr)
# Prevents dead code elimination
str(arrptr[0])
@timefunc("malloc memoryview")
def _(int L):
cdef int i
cdef double* arrptr
cdef double[::1] arr
for i in range(loops):
arrptr = <double*> malloc(sizeof(double) * L)
arr = <double[:L]>arrptr
free(arrptr)
# Prevents dead code elimination
str(arr[0])
@timefunc("cvarray memoryview")
def _(int L):
cdef int i
cdef double[::1] arr
for i in range(loops):
arr = cvarray((L,),sizeof(double),''d'')
# Prevents dead code elimination
str(arr[0])
print()
print("ITERATING")
loops = 1000
@timefunc("cpython.array buffer")
def _(int L):
cdef int i
cdef array[double] arr = clone(array(''d''), L, False)
cdef double d
for i in range(loops):
for i in range(L):
d = arr[i]
# Prevents dead-code elimination
str(d)
@timefunc("cpython.array memoryview")
def _(int L):
cdef int i
cdef double[::1] arr = clone(array(''d''), L, False)
cdef double d
for i in range(loops):
for i in range(L):
d = arr[i]
# Prevents dead-code elimination
str(d)
@timefunc("cpython.array raw C type")
def _(int L):
cdef int i
cdef array arr = clone(array(''d''), L, False)
cdef double d
for i in range(loops):
for i in range(L):
d = arr[i]
# Prevents dead-code elimination
str(d)
@timefunc("numpy.empty_like memoryview")
def _(int L):
cdef int i
cdef double[::1] arr = numpy.empty((L,), dtype=''double'')
cdef double d
for i in range(loops):
for i in range(L):
d = arr[i]
# Prevents dead-code elimination
str(d)
@timefunc("malloc")
def _(int L):
cdef int i
cdef double* arrptr = <double*> malloc(sizeof(double) * L)
cdef double d
for i in range(loops):
for i in range(L):
d = arrptr[i]
free(arrptr)
# Prevents dead-code elimination
str(d)
@timefunc("malloc memoryview")
def _(int L):
cdef int i
cdef double* arrptr = <double*> malloc(sizeof(double) * L)
cdef double[::1] arr = <double[:L]>arrptr
cdef double d
for i in range(loops):
for i in range(L):
d = arr[i]
free(arrptr)
# Prevents dead-code elimination
str(d)
@timefunc("cvarray memoryview")
def _(int L):
cdef int i
cdef double[::1] arr = cvarray((L,),sizeof(double),''d'')
cdef double d
for i in range(loops):
for i in range(L):
d = arr[i]
# Prevents dead-code elimination
str(d)
Salida:
INITIALISATIONS
Running cpython.array buffer
0.100040 0.097140 0.133110 0.121820 0.131630 0.108420 0.112160 μs
Running cpython.array memoryview
0.339480 0.333240 0.378790 0.445720 0.449800 0.414280 0.414060 μs
Running cpython.array raw C type
0.048270 0.049250 0.069770 0.074140 0.076300 0.060980 0.060270 μs
Running numpy.empty_like memoryview
1.006200 1.012160 1.128540 1.212350 1.250270 1.235710 1.241050 μs
Running malloc
0.021850 0.022430 0.037240 0.046260 0.039570 0.043690 0.030720 μs
Running malloc memoryview
1.640200 1.648000 1.681310 1.769610 1.755540 1.804950 1.758150 μs
Running cvarray memoryview
1.332330 1.353910 1.358160 1.481150 1.517690 1.485600 1.490790 μs
ITERATING
Running cpython.array buffer
0.010000 0.027000 0.091000 0.669000 6.314000 64.389000 635.171000 μs
Running cpython.array memoryview
0.013000 0.015000 0.058000 0.354000 3.186000 33.062000 338.300000 μs
Running cpython.array raw C type
0.014000 0.146000 0.979000 9.501000 94.160000 916.073000 9287.079000 μs
Running numpy.empty_like memoryview
0.042000 0.020000 0.057000 0.352000 3.193000 34.474000 333.089000 μs
Running malloc
0.002000 0.004000 0.064000 0.367000 3.599000 32.712000 323.858000 μs
Running malloc memoryview
0.019000 0.032000 0.070000 0.356000 3.194000 32.100000 327.929000 μs
Running cvarray memoryview
0.014000 0.026000 0.063000 0.351000 3.209000 32.013000 327.890000 μs
(La razón para el punto de referencia de "iteraciones" es que algunos métodos tienen características sorprendentemente diferentes a este respecto).
En orden de velocidad de inicialización:
malloc
: Este es un mundo duro, pero es rápido. Si necesita asignar muchas cosas y tener un rendimiento iterativo y de indexación sin obstáculos, tiene que ser así. Pero normalmente eres una buena apuesta para ...
cpython.array raw C type
: Bien, es rápido. Y es seguro. Lamentablemente, atraviesa Python para acceder a sus campos de datos. Puedes evitar eso usando un truco maravilloso:
arr.data.as_doubles[i]
que lo lleva a la velocidad estándar mientras elimina la seguridad. ¡Esto hace que este sea un reemplazo maravilloso para malloc
, siendo básicamente una bonita versión contada de referencia!
cpython.array buffer
: llegando a solo tres o cuatro veces el tiempo de configuración de malloc
, esto es una apuesta maravillosa. Lamentablemente, tiene una sobrecarga significativa (aunque pequeña en comparación con las directivas de boundscheck
y wraparound
). Eso significa que solo realmente compite contra las variantes de seguridad total, pero es el más rápido de los que se inicializan. Tu elección.
cpython.array memoryview
: ahora es un orden de magnitud más lento que malloc
para inicializar. Es una pena, pero se repite igual de rápido. Esta es la solución estándar que sugeriría a menos que estén boundscheck
o el wraparound
(en cuyo caso el cpython.array buffer
podría ser una compensación más convincente).
El resto. El único que vale la pena es numpy
''s, debido a los muchos métodos divertidos asociados a los objetos. Eso es todo, sin embargo.