Pregiudizi su python e numpy

A volte le nostre convinzioni sono basate sul nulla e alla prova dei fatti sono sbagliate.

Il python è un linguaggio interpretato e come tale è lento: è vero ma a volte a complicare
le cose ci si mettono anche i nostri pregiudizi.

Dovevo produrre un vettore e per farlo, nella notte dei tempi, avevo scritto una funzioncina stupida:

def old_grid_generator():
      Psealevel    = 1000.0
      scale_height = 6.0
      Zdummy       = np.concatenate((np.arange(80,55,-2),np.arange(54,4,-1)))
      return Psealevel * np.exp(-Zdummy / scale_height)
Code language: Python (python)

Come si intuisce ho usato la libreria numpy nella convinzione che la libreria, compilata dal C, facesse un buon lavoro. Ma siccome il mio cervello funziona male, qualche tempo fa, riguardandola, mi sono chiesto se questa cosa fosse vera. Allora mi sono messo a pensare 5 minuti a come avrei potuto scrivere del codice “pure python” ragionevolmente veloce, il primo risultato è stato questo:

def old_grid_generator_2():
      Psealevel    = 1000.0
      scale_height = 6.0
      from math import exp
      return ( Psealevel * exp(-zz / scale_height) for zz in (*range(80,55,-2),*range(54,4,-1)) )
Code language: Python (python)

confrontando i tempi che impiegano a girare:

In [302]: %timeit old_grid_generator()
5.38 µs ± 40 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [333]: %timeit old_grid_generator_2()
1.39 µs ± 28 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)Code language: CSS (css)

Meglio se importo exp fuori dalla funzione

In [32]: %timeit old_grid_generator_2()
925 ns ± 9.11 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)Code language: CSS (css)

Ancora meglio se uso la funzione itertools.chain per concatenare 2 generatori senza passare per la tupla, per convenienza ne ho fatto una terza versione.

from itertools import chain
def old_grid_generator_3():
    Psealevel    = 1000.0
    scale_height = 6.0
    return ( Psealevel * exp(-zz / scale_height) for zz in chain(range(80,55,-2),range(54,4,-1)))

In [114]: %timeit old_grid_generator_3()
516 ns ± 2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)Code language: JavaScript (javascript)

Ah, quindi in questo caso particolare il python da solo fa 10 volte meglio di numpy.
Vabbè, mi sono detto, sto barando: la prima versione tira fuori un utilissimo
numpy.array mentre la seconda un banalissimo generatore. Allora:

In [116]: %timeit np.fromiter(old_grid_generator_3(),float)
9.47 µs ± 152 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)Code language: CSS (css)

Viene fuori che se gli faccio costruire il vettore numpy il tempo è 2 volte quello che ci mette la versione nativa numpy: se lo riprende con gli interessi. Quindi scrivere codice che usa le numpy nativamente è più efficiente che scrivere codice veloce in puro python e poi convertire i risultati in numpy.

Allora mi chiedo se a richiedere tempo è il vettore numpy o il fatto di voler “mettere a terra” il risultato immagazzinandolo in memoria.

Per testare questa ipotesi genero una tupla, oggetto decisamente più snello di un array numpy:

In [138]: %timeit tuple(old_grid_generator_3())
7.49 µs ± 29.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)Code language: CSS (css)

Ottengo risultati simili se la tupla la genero all’interno della funzione.

Evidentemente l’operazione che prende tempo è chiedere che i valori vengano immagazzinati in un oggetto in memoria.

Ne deduco che se riuscissimo a ottenere tutti i nostri risultati usando soltanto generatori, senza mai appoggiarli in memoria, se non in ultima istanza, probabilmente otterremmo del codice rapido (confrontabile o quasi coi risultati delle numpy) e molto meno pesante in memoria. Per dirla in modo più sintetico, se scrivessimo il codice in modo un po’ più “funzionale”.

Non sono sicuro che sia sempre fattibile…

Quel che resterebbe da capire è come questo comportamento scali all’aumentare del numero di elementi del vettore.

Questo semplice esempio fa vedere è come in alcuni casi (attenzione a generalizzare) il design progettuale del “nuovo” python3 e quello delle numpy sono in aperto disaccordo.

Ma l’insegnamento più importante è quello di fermarsi un attimo a pensare prima di scrivere del codice, anche quello che svolge lavori banali.