Wednesday, May 6, 2015

VSOP2013 - profiling e scelta dei metodi - 1 parte: rilettura dei file di parametri orbitali

La convinzione diffusa che Python sia un ambiente di sviluppo caratterizzato da lentezza esecutiva, almeno in rapporto ai compilatori tradizionalmente ritenuti più efficienti, come C, C++ e Fortran, è fondata solo in parte.

Giocano sicuramente a sfavore alcune caratteristiche intrinseche del linguaggio, che è fondamentamentalmente un linguaggio di scripting, a tipizzazione dinamica, con alcune strutture dati, quali le liste, che impongono all'interprete grandi spostamenti in memoria, una volta superate certe dimensioni. Per esempio, aggiornare una lista inserendo elementi al suo interno, anzichè accodarli in append, puo' risultare molto pesante quando la lista è costituita da migliaia di termini, dovendosi procedere, ad ogni nuovo inserimento, allo spostamento globale in memoria di tutta la lista. Non è detto pero' che la libreria di base non fornisca metodi utili a evitare sia overhead che massiccio impegno di memoria. Per ogni necessità, possiamo confrontare tra loro più metodi, utilizzando allo scopo degli appositi strumenti di misura, che vanno sotto il nome di profiler.

Oggi continuo il lavoro iniziato nel post precedente, in cui abbiamo ottenuto un dizionario contenente riferimenti ai limiti di inizio e fine di ogni blocco dati all'interno dei file di parametri VSOP2013.

La creazione del dizionario e il suo storage, anche se lenti, sono stati un'operazione da fare una volta sola, della durata di circa un minuto, per cui non mi sono preoccupato particolarmente della velocità esecutiva.

Diverso è il problema della rilettura ed elaborazione dei dati, al fine di ricavare i parametri da sottoporre poi a calcolo per ottenere, alla fine, le variazioni temporali da applicare ai parametri orbitali per gli otto pianeti più il baricentro terra-luna. Non è un compito semplice, voglio arrivare alla fine dell'estrazione ad avere una o più matrici numeriche con cui effettuare i calcoli con i metodi dell'algebra matriciale e, in particolare, con la libreria numpy, le cui routine critiche sono scritte in C e Fortran.

Ho scomposto il lavoro di definizione in più tempi e in più alternative. Oggi provo a scrivere la routine di lettura usando diversi metodi, per confrontarli poi con le funzioni del modulo di misura integrato nella libreria di Python cProfile, che naturalmente dovro' importare nel mio codice, se voglio effettuare le misure dall'interno del codice stesso. Per ovviare alla velocità (comunque elevata) di esecuzione, utilizzo il modulo timeit per ognuna delle tre funzioni

Il listato allegato di seguito, che contiene l'intero programma, usa tre metodi diversi per leggere i file dati (ho scelto quello relativo al pianeta Mercurio, alla sola prima variabile, al primo blocco e a una soglia di arresto pari a 1e-20, praticamente l'intera serie). In questa fase non faccio trattamento delle stringhe per ricavarne i dati numerici, mi accontento di verificare eventuali differenze nella velocità di recupero delle stesse. In coda al listato si trovano una def main() che riepiloga le tre routine e, infine, i metodi di profiling e il timing con il metodo timeit con 100 ripetizioni per ogni funzione.

import cPickle
from itertools import islice
import cProfile, pstats, timeit

limits = cPickle.load(open('VSOP2013_ranges.pickle','rb'))

def timing01():
    chunk = []
    file_in = open('VSOP2013p1.dat')
    all_lines = file_in.readlines()
    start, end = limits[1][1][0][20]
    chunk = all_lines[start:end+1]
    file_in.close()
    return chunk
    
def timing02():
    chunk=[]
    file_in = open('VSOP2013p1.dat')
    start, end = limits[1][1][0][20]
    chunk = [line for line in islice(file_in, start, end+1)]
    file_in.close()    
    return chunk
    
def timing03():
    chunk = []
    file_in = open('VSOP2013p1.dat')
    start, end = limits[1][1][0][20]
    chunk = list(islice(file_in, start, end+1))
    file_in.close()
    return chunk
        
def main():
    result1 = timing01()
    result2 = timing02()
    result3 = timing03()
    return (result1, result2, result3)

if __name__ == '__main__':
    
    profile = cProfile.run("main()","prof.prof")
    p = pstats.Stats("prof.prof")
    p.sort_stats("cumulative").print_stats(15)

    result = main()
    print "timing 100 repliche timing01():", timeit.timeit("timing01()", setup = "from __main__ import timing01", number=100)
    print "timing 100 repliche timing02():", timeit.timeit("timing02()", setup = "from __main__ import timing02", number=100)
    print "timing 100 repliche timing03():", timeit.timeit("timing03()", setup = "from __main__ import timing03", number=100)
    print "-" * 80
    print "primo elemento della lista 1:", result[0][0]
    print "primo elemento della lista 2:", result[1][0]
    print "primo elemento della lista 3:", result[2][0]
    
    print "ultimo elemento della lista 1:", result[0][-1]
    print "ultimo elemento della lista 2:", result[1][-1]
    print "ultimo elemento della lista 3:", result[2][-1]
    

Nel mio PC i risultati sono i seguenti (possono differire in modo significativo su altre macchine):

Python 2.7.9 (default, Apr  2 2015, 15:33:21) 
[GCC 4.9.2] on linux2
Type "copyright", "credits" or "license()" for more information.
>>> ================================ RESTART ================================
>>> 
Wed May  6 17:23:08 2015    prof.prof

         13 function calls in 0.072 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.004    0.004    0.072    0.072 <string>:1(<module>)
        1    0.010    0.010    0.068    0.068 /home/ubuntu/Scrivania/vsop2013/solution/timing.py:32(main)
        1    0.001    0.001    0.047    0.047 /home/ubuntu/Scrivania/vsop2013/solution/timing.py:7(timing01)
        1    0.046    0.046    0.046    0.046 {method 'readlines' of 'file' objects}
        1    0.006    0.006    0.006    0.006 /home/ubuntu/Scrivania/vsop2013/solution/timing.py:16(timing02)
        1    0.006    0.006    0.006    0.006 /home/ubuntu/Scrivania/vsop2013/solution/timing.py:24(timing03)
        3    0.000    0.000    0.000    0.000 {method 'close' of 'file' objects}
        3    0.000    0.000    0.000    0.000 {open}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


timing 100 repliche timing01(): 5.62393903732
timing 100 repliche timing02(): 0.71301984787
timing 100 repliche timing03(): 0.654221773148
--------------------------------------------------------------------------------
primo elemento della lista 1:     1   0  0  0  0   0  0  0  0  0    0   0   0   0      0   0  0  0  0.0000000000000000 +00  0.3870983098840000 +00

primo elemento della lista 2:     1   0  0  0  0   0  0  0  0  0    0   0   0   0      0   0  0  0  0.0000000000000000 +00  0.3870983098840000 +00

primo elemento della lista 3:     1   0  0  0  0   0  0  0  0  0    0   0   0   0      0   0  0  0  0.0000000000000000 +00  0.3870983098840000 +00

ultimo elemento della lista 1: 32240   3  0  0  0   0  0  0  0 -7    0   0   0   0      0   0  0  0 -0.7232436513837180 -16 -0.4617790227683706 -15

ultimo elemento della lista 2: 32240   3  0  0  0   0  0  0  0 -7    0   0   0   0      0   0  0  0 -0.7232436513837180 -16 -0.4617790227683706 -15

ultimo elemento della lista 3: 32240   3  0  0  0   0  0  0  0 -7    0   0   0   0      0   0  0  0 -0.7232436513837180 -16 -0.4617790227683706 -15

>>> 

E' abbastanza evidente il pessimo timing della prima funzione: 8 volte più lenta e inefficiente delle successive. Inoltre, la necessità di creare una lista per procedere al suo slicing, oltre a comportare un esagerato impegno di memoria, si rivela impegnativa anche sotto il profilo dei tempi di esecuzione, giustificando la quasi totalità del tempo impiegato dalla funzione. Le due funzioni successive sono decisamente più performanti, a dimostrare che una scelta accurata dei metodi puo' essere determinante, anche quando usiamo funzioni di libreria.

Ho previsto la stampa del primo e dell'ultimo elemento del blocco ottenuto da ogni funzione per mostrare che i risultati prodotti dalle funzioni sono identici. Nel post successivo utilizzeremo il metodo islice per l'estrazione delle righe del file dei parametri e inizieremo, sempre confrontando più metodi, a convertire le righe di testo in variabili numeriche.

No comments:

Post a Comment

How to create a virtual linux machine with qemu under Debian or Ubuntu with near native graphics performance

It's been a long time since my latest post. I know, I'm lazy. But every now and then I like to publish something that other people c...