2014-12-13 13 views
14

Ho finalmente trovato un collo di bottiglia nelle prestazioni nel mio codice ma sono confuso su quale sia la ragione. Per risolverlo ho cambiato tutte le mie chiamate di numpy.zeros_like per utilizzare invece numpy.zeros. Ma perché lo zeros_like è così tanto più lento?Perché la differenza di prestazioni tra numpy.zeros e numpy.zeros_like?

Per esempio (nota e-05 sulla chiamata zeros):

>>> timeit.timeit('np.zeros((12488, 7588, 3), np.uint8)', 'import numpy as np', number = 10) 
5.2928924560546875e-05 
>>> timeit.timeit('np.zeros_like(x)', 'import numpy as np; x = np.zeros((12488, 7588, 3), np.uint8)', number = 10) 
1.4402990341186523 

Ma poi stranamente scrivendo a una matrice creata con zeros è notevolmente più lento di una serie creata con zeros_like:

>>> timeit.timeit('x[100:-100, 100:-100] = 1', 'import numpy as np; x = np.zeros((12488, 7588, 3), np.uint8)', number = 10) 
0.4310588836669922 
>>> timeit.timeit('x[100:-100, 100:-100] = 1', 'import numpy as np; x = np.zeros_like(np.zeros((12488, 7588, 3), np.uint8))', number = 10) 
0.33325695991516113 

mio suppongo che zeros stia usando qualche trucco della CPU e non scriva effettivamente nella memoria per allocarlo. Questo è fatto al volo quando è scritto. Ma questo non spiega ancora la grande discrepanza nei tempi di creazione dell'array.

che sto con Mac OS X Yosemite con la versione corrente NumPy:

>>> numpy.__version__ 
'1.9.1' 
+0

'zeros' usa' memset'; 'zeros_like' sembra fare un' fill' che fa un sacco di sciocchezze. Ho provato a inseguire la vera esecuzione ma è inutilmente ottusa. – Veedrac

+0

'zeri' non usa memset. –

risposta

10

miei tempi in ipython sono (con un'interfaccia più semplice timeit):

In [57]: timeit np.zeros_like(x) 
1 loops, best of 3: 420 ms per loop 

In [58]: timeit np.zeros((12488, 7588, 3), np.uint8) 
100000 loops, best of 3: 15.1 µs per loop 

Quando guardo il codice con IPython (np.zeros_like??) vedo:

res = empty_like(a, dtype=dtype, order=order, subok=subok) 
multiarray.copyto(res, 0, casting='unsafe') 

mentre np.zeros è una blackbox: puro codice compilato.

tempi per empty sono:

In [63]: timeit np.empty_like(x) 
100000 loops, best of 3: 13.6 µs per loop 

In [64]: timeit np.empty((12488, 7588, 3), np.uint8) 
100000 loops, best of 3: 14.9 µs per loop 

Così il tempo supplementare in zeros_like è in quella copy.

Nei miei test, la differenza nei tempi di assegnazione (x[]=1) è trascurabile.

La mia ipotesi è che zeros, ones, empty sono tutte le prime creazioni compilate. empty_like è stato aggiunto per comodità, basta disegnare forma e digitare informazioni dal suo input. zeros_like è stato scritto con un occhio di riguardo per una facile manutenzione della programmazione (riutilizzo empty_like) rispetto alla velocità.

np.ones e np.full utilizzare anche la sequenza np.empty ... copyto e mostrare tempi simili.


https://github.com/numpy/numpy/blob/master/numpy/core/src/multiarray/array_assign_scalar.c sembra essere di file che copia uno scalari (come 0) ad un array. Non vedo l'uso di memset.

https://github.com/numpy/numpy/blob/master/numpy/core/src/multiarray/alloc.c ha chiamate a malloc e calloc.

https://github.com/numpy/numpy/blob/master/numpy/core/src/multiarray/ctors.c - fonte per zeros e empty.Entrambi chiamano PyArray_NewFromDescr_int, ma uno finisce usando npy_alloc_cache_zero e l'altro npy_alloc_cache.

npy_alloc_cache in alloc.c chiamate alloc. npy_alloc_cache_zero chiama npy_alloc_cache seguito da un memset. Il codice alloc.c viene ulteriormente confuso con un'opzione THREAD.

Maggiori informazioni sul calloc v malloc+memset differenza: Why malloc+memset is slower than calloc?

Ma con il caching e la raccolta dei rifiuti, mi chiedo se vale la distinzione calloc/memset.


Questo semplice test con il pacchetto memory_profile supporta l'affermazione che zeros e empty allocare memoria 'on-the-fly', mentre zeros_like assegna tutto in anticipo:

N = (1000, 1000) 
M = (slice(None, 500, None), slice(500, None, None)) 

Line # Mem usage Increment Line Contents 
================================================ 
    2 17.699 MiB 0.000 MiB @profile 
    3        def test1(N, M): 
    4 17.699 MiB 0.000 MiB  print(N, M) 
    5 17.699 MiB 0.000 MiB  x = np.zeros(N) # no memory jump 
    6 17.699 MiB 0.000 MiB  y = np.empty(N) 
    7 25.230 MiB 7.531 MiB  z = np.zeros_like(x) # initial jump 
    8 29.098 MiB 3.867 MiB  x[M] = 1  # jump on usage 
    9 32.965 MiB 3.867 MiB  y[M] = 1 
    10 32.965 MiB 0.000 MiB  z[M] = 1 
    11 32.965 MiB 0.000 MiB  return x,y,z 
+0

Dare un'occhiata agli interni era la giusta direzione da seguire. Strano che "zeros_like" non invochi semplicemente gli "zeri". Ho eseguito i test di assegnazione più volte e ottengo sempre piccole differenze ma tempi notevolmente più veloci per gli array 'zeros_like'. –

+3

Le supposizioni di cui sopra non sono la ragione. La differenza effettiva è se l'azzeramento della memoria è lasciato al sottosistema VM del sistema operativo, o fatto dal processo stesso. –

+0

La spiegazione di 'calloc/memset' sembra logica, ma ho problemi a confermarlo nel codice' numpy'. – hpaulj

10

OS moderni allocano memoria virtualmente Ad esempio, la memoria viene data a un processo solo quando viene utilizzata per la prima volta. zeros ottiene la memoria dal sistema operativo in modo che il sistema operativo lo azzeri quando viene utilizzato per la prima volta. zeros_like d'altra parte riempie la memoria allocata con zeri di per sé. Entrambi i modi richiedono circa la stessa quantità di lavoro --- è solo che con zeros_like l'azzeramento è fatto in anticipo, mentre zeros finisce per farlo al volo.

Tecnicamente, in C la differenza è chiamando calloc rispetto a malloc+memset.

Problemi correlati