python tutorial Cython: sono le visualizzazioni di memoria digitate il modo moderno di digitare array numpy?



scipy (1)

Diciamo che mi piacerebbe passare una matrice numpy a una funzione cdef :

cdef double mysum(double[:] arr):
    cdef int n = len(arr)
    cdef double result = 0

    for i in range(n):
        result = result + arr[i]

    return result

È questo il modo moderno di gestire gli array numpy digitati? Confronta con questa domanda: tipo cython / numpy di un array

Cosa succede se voglio fare quanto segue:

cdef double[:] mydifference(int a, int b):
    cdef double[:] arr_a = np.arange(a)
    cdef double[:] arr_b = np.arange(b)

    return arr_a - arr_b

Ciò restituirà un errore perché - non è definito per memoryviews. Quindi, se quel caso fosse stato gestito come segue?

cdef double[:] mydifference(int a, int b):
    arr_a = np.arange(a)
    arr_b = np.arange(b)

    return arr_a - arr_b

Citerò dai documenti i documenti

Le visualizzazioni di memoria sono simili all'attuale supporto del buffer di array NumPy ( np.ndarray[np.float64_t, ndim=2] ), ma hanno più funzioni e una sintassi più pulita.

Ciò indica che gli sviluppatori di Cython considerano le visualizzazioni di memoria come moderne.

Le visualizzazioni di memoria offrono alcuni grandi vantaggi rispetto alla notazione np.ndarray principalmente in eleganza e interoperabilità, tuttavia non sono superiori nelle prestazioni.

Prestazione:

Innanzitutto va notato che boundscheck a volte non riesce a funzionare con le visualizzazioni di memoria risultanti in cifre artificialmente veloci per le visualizzazioni di memoria con boundscheck = True (cioè si ottiene l'indicizzazione veloce e non sicura), se si sta facendo affidamento su boundscheck per catturare bug questo potrebbe essere un brutto sorpresa.

Per la maggior parte, una volta applicate le ottimizzazioni del compilatore, le visualizzazioni di memoria e la notazione di array numpy sono uguali in termini di prestazioni, spesso proprio così. Quando c'è una differenza, normalmente non supera il 10-30%.

Benchmark delle prestazioni

Il numero è il tempo in secondi per eseguire 100.000.000 di operazioni. Più piccolo è più veloce.

ACCESS+ASSIGNMENT on small array (10000 elements, 10000 times)
Results for `uint8`
1) memory view: 0.0415 +/- 0.0017
2) np.ndarray : 0.0531 +/- 0.0012
3) pointer    : 0.0333 +/- 0.0017

Results for `uint16`
1) memory view: 0.0479 +/- 0.0032
2) np.ndarray : 0.0480 +/- 0.0034
3) pointer    : 0.0329 +/- 0.0008

Results for `uint32`
1) memory view: 0.0499 +/- 0.0021
2) np.ndarray : 0.0413 +/- 0.0005
3) pointer    : 0.0332 +/- 0.0010

Results for `uint64`
1) memory view: 0.0489 +/- 0.0019
2) np.ndarray : 0.0417 +/- 0.0010
3) pointer    : 0.0353 +/- 0.0017

Results for `float32`
1) memory view: 0.0398 +/- 0.0027
2) np.ndarray : 0.0418 +/- 0.0019
3) pointer    : 0.0330 +/- 0.0006

Results for `float64`
1) memory view: 0.0439 +/- 0.0037
2) np.ndarray : 0.0422 +/- 0.0013
3) pointer    : 0.0353 +/- 0.0013

ACCESS PERFORMANCE (100,000,000 element array):
Results for `uint8`
1) memory view: 0.0576 +/- 0.0006
2) np.ndarray : 0.0570 +/- 0.0009
3) pointer    : 0.0061 +/- 0.0004

Results for `uint16`
1) memory view: 0.0806 +/- 0.0002
2) np.ndarray : 0.0882 +/- 0.0005
3) pointer    : 0.0121 +/- 0.0003

Results for `uint32`
1) memory view: 0.0572 +/- 0.0016
2) np.ndarray : 0.0571 +/- 0.0021
3) pointer    : 0.0248 +/- 0.0008

Results for `uint64`
1) memory view: 0.0618 +/- 0.0007
2) np.ndarray : 0.0621 +/- 0.0014
3) pointer    : 0.0481 +/- 0.0006

Results for `float32`
1) memory view: 0.0945 +/- 0.0013
2) np.ndarray : 0.0947 +/- 0.0018
3) pointer    : 0.0942 +/- 0.0020

Results for `float64`
1) memory view: 0.0981 +/- 0.0026
2) np.ndarray : 0.0982 +/- 0.0026
3) pointer    : 0.0968 +/- 0.0016

ASSIGNMENT PERFORMANCE (100,000,000 element array):
Results for `uint8`
1) memory view: 0.0341 +/- 0.0010
2) np.ndarray : 0.0476 +/- 0.0007
3) pointer    : 0.0402 +/- 0.0001

Results for `uint16`
1) memory view: 0.0368 +/- 0.0020
2) np.ndarray : 0.0368 +/- 0.0019
3) pointer    : 0.0279 +/- 0.0009

Results for `uint32`
1) memory view: 0.0429 +/- 0.0022
2) np.ndarray : 0.0427 +/- 0.0005
3) pointer    : 0.0418 +/- 0.0007

Results for `uint64`
1) memory view: 0.0833 +/- 0.0004
2) np.ndarray : 0.0835 +/- 0.0011
3) pointer    : 0.0832 +/- 0.0003

Results for `float32`
1) memory view: 0.0648 +/- 0.0061
2) np.ndarray : 0.0644 +/- 0.0044
3) pointer    : 0.0639 +/- 0.0005

Results for `float64`
1) memory view: 0.0854 +/- 0.0056
2) np.ndarray : 0.0849 +/- 0.0043
3) pointer    : 0.0847 +/- 0.0056

Codice di riferimento (mostrato solo per access + assegnazione)

# cython: boundscheck=False
# cython: wraparound=False
# cython: nonecheck=False
import numpy as np
cimport numpy as np
cimport cython

# Change these as desired.
data_type = np.uint64
ctypedef np.uint64_t data_type_t

cpdef test_memory_view(data_type_t [:] view):
    cdef Py_ssize_t i, j, n = view.shape[0]

    for j in range(0, n):
        for i in range(0, n):
            view[i] = view[j]

cpdef test_ndarray(np.ndarray[data_type_t, ndim=1] view):
    cdef Py_ssize_t i, j, n = view.shape[0]

    for j in range(0, n):
        for i in range(0, n):
            view[i] = view[j]

cpdef test_pointer(data_type_t [:] view):
    cdef Py_ssize_t i, j, n = view.shape[0]
    cdef data_type_t * data_ptr = &view[0]

    for j in range(0, n):
        for i in range(0, n):
            (data_ptr + i)[0] = (data_ptr + j)[0]

def run_test():
    import time
    from statistics import stdev, mean
    n = 10000
    repeats = 100
    a = np.arange(0, n,  dtype=data_type)
    funcs = [('1) memory view', test_memory_view),
        ('2) np.ndarray', test_ndarray),
        ('3) pointer', test_pointer)]

    results = {label: [] for label, func in funcs}
    for r in range(0, repeats):
        for label, func in funcs:
            start=time.time()
            func(a)
            results[label].append(time.time() - start)

    print('Results for `{}`'.format(data_type.__name__))
    for label, times in sorted(results.items()):
        print('{: <14}: {:.4f} +/- {:.4f}'.format(label, mean(times), stdev(times)))

Questi benchmark indicano che nel complesso non c'è molta differenza nelle prestazioni. A volte la notazione np.ndarray è un po 'più veloce, e talvolta vice-verca.

Una cosa a cui fare attenzione con benchmark è che quando il codice è reso un po 'più complicato o "realistico" la differenza improvvisamente svanisce, come se il compilatore perdesse fiducia nell'applicare un'ottimizzazione molto intelligente. Questo può essere visto con le prestazioni dei float dove non c'è alcuna differenza, presumibilmente, dal momento che alcune fantasiose ottimizzazioni intere non possono essere usate.

Facilità d'uso

Le viste di memoria offrono vantaggi significativi, ad esempio è possibile utilizzare una vista memoria su array numpy, array CPython, array cython, array c e altro, sia presente che futuro. C'è anche la semplice sintassi parallela per trasmettere qualsiasi cosa a una vista di memoria:

cdef double [:, :] data_view = <double[:256, :256]>data

Le visualizzazioni di memoria sono grandiose in questo senso, perché se si digita una funzione come una vista memoria allora può prendere una di quelle cose. Questo significa che puoi scrivere un modulo che non ha una dipendenza da numpy, ma che può comunque prendere array numpy.

D'altra parte, la notazione di np.ndarray risulta in qualcosa che è ancora una matrice numpy e puoi richiamare tutti i metodi di array numpy su di essa. Non è un grosso problema avere una matrice numpy e una vista sull'array però:

def dostuff(arr):
    cdef double [:] arr_view = arr
    # Now you can use 'arr' if you want array functions,
    # and arr_view if you want fast indexing

Avere sia la matrice che la vista dell'array funziona in pratica e mi piace molto lo stile, in quanto distingue chiaramente i metodi a livello di python e quelli di livello C.

Conclusione

Le prestazioni sono quasi uguali e non c'è certamente abbastanza differenza per essere un fattore decisivo.

La notazione numpy dell'array si avvicina all'ideale di accelerare il codice Python senza cambiarlo molto, poiché è possibile continuare a utilizzare la stessa variabile, ottenendo allo stesso tempo l'indicizzazione di array a velocità massima.

D'altra parte, la notazione della vista di memoria probabilmente è il futuro. Se ti piace l'eleganza di esso, e usi diversi tipi di contenitori di dati che solo array numpy, c'è una buona ragione per usare le viste di memoria per motivi di coerenza.





cython