Smonta, isola ed ottimizza…

Fermo restando che, come direbbe Knuth,

“premature optimization is the root of all evil.”

ciò non significa assolutamente che non si possano nè di debba ad un certo momento pensare a come far funzionare le cose in modo “migliore”.

Si giunge quindi al problema del cosa ottimizzare e come procedere?

Questo è il cuore dell’ottimizzazione, la ricerca di modi via via più efficienti di fare qualcosa.

Tipi di ottimizzazione

E’ risaputo che esistano principalmente due tipi di ottimizzazioni, uno a livello algoritmico ed uno a livello implementativo.

Dei due il primo è il più importante, quello che garantisce i migliori risultati ma, sfortunatamente, è anche quello più complicato. Se ad un problema corrispondono più soluzioni, sceglierne una ottimale rappresenta la parte più complessa dell’impresa.

Significa approcciarsi in modi anche profondamente diversi ad un problema e ricavarne uno o più algoritmi risolutivi, magari in competizione fra loro per la palma dell’ottimalità. O rivederne altri per migliorarli.

Sfortunatamente, nel mondo industriale e salvo eccezioni specifiche, non si ha mai il tempo di trovare e provare fra loro più soluzioni: questa è una differenza significativa fra il mondo accademico e quello aziendale simboleggiato dallo slogan-obiettivo del “get the things done!“.

Di conseguenza, se si sta lavorando su qualcosa da vendere, è bene “azzeccare” fin da subito (almeno) una soluzione decente e poi migliorarla se e quando possibile: dati certi vincoli, una scelta data per acquisita sarà in seguito difficilmente rivedibile o ritrattabile.

A quel punto le “teoricamente infinite possibilità di ottimizzazione teorica” scemano in modo netto: ben presto tutto si trasforma in un mero “come velocizzare questo codice?“.

Ottimizzare le singole parti

Personalmente, specie quando il tempo è agli sgoccioli e non c’è tempo per aggredire nuovamente il problema in blocco dal punto di vista algoritmico, sono per un approccio di “ottimizzazione per sostituzione di porzioni con porzioni di codice“.

Dato per scontato che un certo codice rappresenta per ovvi motivi un’implementazione per compromesso di un certo algoritmo (cioè il passaggio da carta a computer significa doversi abbassare “a quello che passa il convento”: linguaggio, librerie, …), la mia strategia consiste cioè in un’ottimizzazione preceduta da un’opportuna fase di divide et impera.

Dopo aver individuato le porzioni del codice particolamente pesanti (tramite profiling, verifica dei tempi di esecuzione, benchmark, …) e che presentano margini significativi di miglioramento, le isolo, magari “rinchiudendo” le relative righe in funzioni separate.

Conosco già le obiezioni: “più funzioni equivale ad overhead dovuti a nuovi e maggiori costi di chiamata“. Forse era così una volta: i compilatori odierni non sono idioti e sono perfettamente in grado di rendere “inline” quanto più codice possibile, quando ritengono che sia conveniente farlo.

La tecnica dello smontare e isolare facilita l’approccio successivo, il “drop-in replacement“, ossia la possibilità di sostituire singole funzioni con delle alternative ottimizzate, nonchè la possibilità di testare il tutto passo-dopo-passo (*).

Si tratta dunque di una forma di refactoring a tutti gli effetti.

Dopotutto se vige la regola aurea del “una funzione, uno scopo“, ottimizzarne una significa migliorare un sottoproblema di un problema più grande.

Questo tipo di ottimizzazione è certamente qualitativamente inferiore, a livello teorico, rispetto ad un approccio globale ma, come si insegna in Uni, un ottimo locale è più facilmente ottenibile (e dimostrabile) di uno globale.

Procedere per gradi

Il processo di sostituzione deve essere graduale: deve essere l’equivalente informatico di una trasformazione fisica quasi-statica in cui ogni nuovo stato è in equilibrio col precedente. In altre parole ogni ottimizzazione deve garantire maggiore efficienza a parità di tutto il resto.

Il mio consiglio spassionato, dopo aver isolato l’isolabile, è quindi concentrarsi e ottimizzare una sola funzione per volta. L’ottimizzazione multipla e contemporanea può perfino essere – e spesso lo è – peggio di quella anticipata.

Lo dico esplicitamente perchè è davvero troppo facile cadere nella trappola del voler migliorare molto o addirittura tutto e per giunta in blocco: è la prima avvisaglia della sindrome dell’ottimizzatore ossessivo-compulsivo. Non ha senso provare a migliorare ogni cosa!

In ogni caso è sempre bene agire su copie di quello che funziona, mai cedere alla tentazione di agire direttamente sugli originali (fra l’altro si perde il termine di paragone!) ed inoltre è garantirsi la reversibilità delle modifiche tramite un buon sistema di versioning (es: Subversion, Git, Mercurial, …).

Che ne pensate?

(*): probabilmente questo discorso apparirà terribilmente banale, ovvio. Per esperienza, non lo è mai. Specie se si ha a che fare con codice altrui e/o che “funziona per magia” al punto che non lo si tocca per paura di romperlo o per pura scaramanzia. Anche in quel caso la tecnica dello “smonta, isola ed ottimizza” spesso paga bene.

Contrassegnato da tag , ,

6 thoughts on “Smonta, isola ed ottimizza…

  1. contezero74 scrive:

    Ciao JP, stranamente questa volta mi trovi perfettamente d’accordo su tutto ;) Strano vero?

    cheers

  2. contezero74 scrive:

    P.S. Ah… mi faccio un po’ di auto-pubblicità ;)
    Anche se spesso più che ottimizzare in se e per se, sarebbe “cosa buona e giusta” considerare l’architettura su cui si sta operando (intesa sia come HW che come insieme compilatore/librerie), perché a parità di codice ci possono inattese sorprese ;) Ti rimando ad uno dei miei ultimi post per maggiori informazioni ;)

  3. Stefano scrive:

    D’accordissimo con il tuo approccio JP e, sì, non è mai un argomento così banale e scontato.
    Se posso, vorrei aggiungere un paio di punti.

    1. Con tempo e risorse a disposizione, sarebbe opportuno che ogni porzione di codice preventivamente isolata sia dotata di test automatici allo scopo di prevenire le eventuali regressioni causate dall’ottimizzazione. Avere un codice più veloce ma che introduce dei bug non è esattamente quello che vogliamo fare .. :)

    2. Avendo isolato e misurato le performance di ogni singola porzione di codice, l’ottimizzazione dovrebbe quasi sempre essere mirata al ‘collo di bottiglia’, ovvero alla porzione al momento più lenta. Una volta ottimizzato il codice della singola porzione, sarà opportuno ripetere nuovamente il profiling (a volte ci sono delle sorprese) e individuare il successivo collo di bottiglia, e così via ..

  4. jp scrive:

    contezero74: LOL! Sì, in effetti è strana come cosa, cioè che concordi. In realtà il post è una premessa per un altro, più pratico, che spero di concludere a breve. Quanto al tuo, bel post davvero (commentato)… :D

    Stefano: esattamente. Ho saputo di persone in perenne lotta con le ottimizzazioni, al punto da voler sistemare anche quello che già funzionava bene. Quando invece il problema principale riguarda i colli di bottiglia… Concordo anche con l’idea dei test, più o meno automatici: ogni porzione di codice sostituita deve soddisfare tutti i requisiti senza introdurre nuovi bug

    Ciao & grazie per essere passati! :D

  5. JustB scrive:

    Grazie per questa analisi jp :)
    Molto apprezzata.

  6. jp scrive:

    @JustB: di nulla! Grazie per essere passato! ^^

Lascia un Commento

Fill in your details below or click an icon to log in:

Logo WordPress.com

You are commenting using your WordPress.com account. Log Out / Modifica )

Foto Twitter

You are commenting using your Twitter account. Log Out / Modifica )

Foto di Facebook

You are commenting using your Facebook account. Log Out / Modifica )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 259 other followers