Le documentation Cython sur les vues de mémoire typée énumère trois façons d'assigner à une vue de mémoire typée:
np.ndarray
etcython.view.array
.Supposons que je n'ai pas de données transmises à ma fonction cython de l'extérieur mais que je souhaite à la place allouer de la mémoire et la renvoyer comme np.ndarray
, laquelle de ces options ai-je choisie? Supposons également que la taille de ce tampon n'est pas une constante de compilation, c'est-à-dire que je ne peux pas allouer sur la pile, mais qu'il faudrait malloc
pour l'option 1.
Les 3 options ressembleraient donc à ceci:
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")
Ce qui m'étonne, c'est que dans les trois cas, Cython génère pas mal de code pour l'allocation mémoire, en particulier un appel à __Pyx_PyObject_to_MemoryviewSlice_dc_int
. Cela suggère (et je peux me tromper ici, ma compréhension du fonctionnement interne de Cython est très limitée) qu'il crée d'abord un objet Python puis le "jette" dans une vue mémoire, ce qui semble frais généraux inutiles.
Un benchmark simple ne révèle pas beaucoup de différence entre les trois méthodes, 2. étant la plus rapide par une marge fine.
Laquelle des trois méthodes est recommandée? Ou existe-t-il une option différente et meilleure?
Question de suivi: Je veux enfin retourner le résultat sous la forme d'un np.ndarray
, après avoir travaillé avec cette vue mémoire dans la fonction. Une vue de mémoire tapée est-elle le meilleur choix ou est-ce que je préfère simplement utiliser l'ancienne interface de tampon comme ci-dessous pour créer un ndarray
en premier lieu?
cdef np.ndarray[DTYPE_t, ndim=1] b = np.empty(N, dtype=np.int32)
Regardez ici pour une réponse.
L'idée de base est que vous voulez cpython.array.array
et cpython.array.clone
(( pas 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)
[~ # ~] modifier [~ # ~]
Il s'avère que les repères de ce fil étaient des ordures. Voici mon set, avec mes horaires:
# 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)
Production:
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 raison du repère "itérations" est que certaines méthodes ont des caractéristiques étonnamment différentes à cet égard.)
Par ordre de vitesse d'initialisation:
malloc
: C'est un monde difficile, mais c'est rapide. Si vous devez allouer beaucoup de choses et avoir des performances d'itération et d'indexation sans entrave, ce doit être ça. Mais normalement, vous êtes un bon pari pour ...
cpython.array raw C type
: Bon sang, c'est rapide. Et c'est sûr. Malheureusement, il passe par Python pour accéder à ses champs de données. Vous pouvez éviter cela en utilisant une merveilleuse astuce:
arr.data.as_doubles[i]
ce qui l'amène à la vitesse standard tout en supprimant la sécurité! Cela en fait un merveilleux remplacement de malloc
, étant fondamentalement une jolie version comptant les références!
cpython.array buffer
: Entrant à seulement trois à quatre fois le temps d'installation de malloc
, c'est un pari merveilleux. Malheureusement, il a une surcharge importante (quoique petite par rapport aux directives boundscheck
et wraparound
). Cela signifie qu'il ne rivalise vraiment qu'avec les variantes à sécurité totale, mais c'est c'est le plus rapide de ceux à initialiser. Votre choix.
cpython.array memoryview
: C'est maintenant un ordre de grandeur plus lent que malloc
pour initialiser. C'est dommage, mais ça se répète tout aussi vite. C'est la solution standard que je suggérerais à moins que boundscheck
ou wraparound
soient activés (auquel cas cpython.array buffer
pourrait être un compromis plus convaincant).
Le reste. Le seul qui vaille la peine est celui de numpy
, en raison des nombreuses méthodes amusantes attachées aux objets. C'est tout, cependant.
Pour faire suite à la réponse de Veedrac: soyez conscient en utilisant le support memoryview
de cpython.array
avec python 2.7 semble entraîner des fuites de mémoire actuellement. Cela semble être un problème de longue date car il est mentionné sur la liste de diffusion des utilisateurs de cython ici dans un article de novembre 2012. L'exécution du script de référence de Veedrac avec Cython version 0.22 avec les deux Python 2.7.6 et Python 2.7.9 entraîne une fuite de mémoire importante) lors de l'initialisation d'un cpython.array
en utilisant une interface buffer
ou memoryview
. Aucune fuite de mémoire ne se produit lors de l'exécution du script avec Python 3.4. J'ai déposé un rapport de bogue à ce sujet dans la liste de diffusion des développeurs Cython.