Ottimizzazione Del Codice
- Definizione e Proprietà
- Tipi e Livelli di Ottimizzazione
- Cosa Ottimizzare
- Buoni e Cattivi Risultati di Ottimizzazione
- Conclusione
- PVS-Studio
Contenuto
Definizione e Proprietà
ottimizzazione del Codice è un metodo di modifica del codice per migliorare la qualità del codice e l’efficienza. Un programma può essere ottimizzato in modo che diventi più piccolo, consumi meno memoria, esegua più rapidamente o esegua meno operazioni di input/output.
I metodi di ottimizzazione dei requisiti di base dovrebbero essere conformi, è che un programma ottimizzato deve avere lo stesso output e gli stessi effetti collaterali della sua versione non ottimizzata. Questo requisito, tuttavia, può essere ignorato nel caso in cui il beneficio dell’ottimizzazione sia stimato più importante delle probabili conseguenze di un cambiamento nel comportamento del programma.
Tipi e livelli di ottimizzazione
L’ottimizzazione può essere eseguita da ottimizzatori automatici o programmatori. Un ottimizzatore è uno strumento software specializzato o un’unità integrata di un compilatore (il cosiddetto compilatore di ottimizzazione). I processori moderni possono anche ottimizzare l’ordine di esecuzione delle istruzioni del codice.
Le ottimizzazioni sono classificate in ottimizzazioni di alto livello e di basso livello. Le ottimizzazioni di alto livello vengono solitamente eseguite dal programmatore, che gestisce entità astratte (funzioni, procedure, classi, ecc.) e tiene presente il quadro generale del compito di ottimizzare la progettazione di un sistema. Ottimizzazioni eseguite a livello di blocchi strutturali elementari di codice sorgente-loop, rami, ecc. – sono solitamente indicati come ottimizzazioni di alto livello, mentre alcuni autori li classificano in un livello separato (“medio”) (N. Wirth?). Le ottimizzazioni di basso livello vengono eseguite nella fase in cui il codice sorgente viene compilato in una serie di istruzioni della macchina, ed è in questa fase che viene solitamente utilizzata l’ottimizzazione automatica. I programmatori assemblatori credono tuttavia che nessuna macchina, per quanto perfetta, possa farlo meglio di un programmatore esperto (eppure tutti concordano sul fatto che un programmatore povero farà molto peggio di un computer).
Cosa ottimizzare
Con l’ottimizzazione manuale del codice, si affronta un altro problema: non è solo necessario sapere come dovrebbe essere eseguita esattamente l’ottimizzazione, ma anche quale parte particolare del programma dovrebbe essere ottimizzata. A causa di vari motivi (lentezza delle operazioni, la differenza nella velocità di lavoro di un operatore umano e un computer, e così via), il 90% del tempo di esecuzione di un programma viene eseguito solo il 10% del codice (questa affermazione è piuttosto speculativo, con il principio di Pareto come abbastanza dubbioso terra, ma A. Tanenbaum rende il suono convincente). Poiché l’ottimizzazione richiede tempo aggiuntivo a parte il tempo speso per lo sviluppo del programma, è meglio concentrarsi sull’ottimizzazione di questo 10% di codice critico rispetto al tempo piuttosto che cercare di ottimizzare l’intero programma. Questi frammenti di codice sono noti come colli di bottiglia e possono essere rilevati da speciali utilità – profiler – che possono misurare il tempo impiegato da varie parti del programma per l’esecuzione.
In pratica, tuttavia, l’ottimizzazione viene solitamente eseguita dopo la fase di programmazione “caotica” (inclusi metodi come “Copia-Incolla”, “vedremo più avanti”, “va bene così”), e quindi è una miscela di ottimizzazione in quanto tale, refactoring e correzioni di bug: semplificazione di costrutti “queer” come strlen(path.c_str ()), condizioni logiche come (a.x != 0 && a.x != 0), e così via. I profiler sono di scarso aiuto con questo tipo di ottimizzazione. Tuttavia, è possibile rilevare questi problemi con strumenti di analisi statica, cioè strumenti progettati per cercare errori semantici, basandosi su un’analisi approfondita del codice sorgente. Come puoi vedere dall’esempio sopra menzionato con la strana condizione, il codice inefficiente può apparire come risultato di errori (come un errore di stampa nel nostro esempio, dove a.x != 0 && a.a != 0 dovrebbe essere invece). Un potente analizzatore statico rileverà tali frammenti di codice e attirerà l’attenzione su di essi producendo messaggi di avviso.
Buoni e cattivi risultati di ottimizzazione
Nella programmazione, quasi tutto dovrebbe essere trattato dal punto di vista della razionalità – l’ottimizzazione non fa eccezione. C’è la convinzione che il codice scritto da un programmatore Assemblatore inesperto sia 3-5 volte più lento del codice generato dal compilatore (Zubkov). Ampiamente conosciuta è una frase di Knuth riguardante le prime ottimizzazioni di basso livello (come i tentativi di risparmiare su operatori o variabili): “L’ottimizzazione prematura è la radice di tutti i mali”.
La maggior parte dei programmatori non si lamenta delle ottimizzazioni eseguite dall’ottimizzatore, alcune delle quali sono convenzionali e obbligatorie. Come, ad esempio, l’ottimizzazione delle chiamate di coda nei linguaggi funzionali (la chiamata di coda è un caso speciale di ricorsione, che può essere rappresentato come un ciclo).
Tuttavia, si dovrebbe capire che più ottimizzazioni complesse a livello di codice macchina possono causare un grande rallentamento della compilazione. Il vantaggio che ti consentono di ottenere potrebbe essere troppo insignificante, rispetto alle ottimizzazioni generali di progettazione del sistema (Wirth). Si dovrebbe anche tenere presente che i linguaggi moderni, con tutti i loro “fronzoli” sintattici e semantici, hanno molte sfumature e sottigliezze, così che un programmatore che non ha familiarità con loro può essere sorpreso da un risultato di ottimizzazione.
Ad esempio, prendere C++ e la cosiddetta Ottimizzazione del valore di ritorno, quando il compilatore evita di copiare un oggetto temporaneo restituito da una funzione. Poiché il compilatore omette la copia, questo metodo viene anche chiamato “Copia elisione”. Quindi, il seguente codice:
#include <iostream> struct C { C() {} C(const C&) { std::cout << "A copy was made.\n"; }}; C f() { return C();} int main() { std::cout << "Hello World!\n"; C obj = f();}
può avere diverse uscite:
Hello World!A copy was made.A copy was made.Hello World!A copy was made.Hello World!
Per quanto possa sembrare strano, tutte e tre le versioni sono valide perché lo standard del linguaggio consente di omettere le chiamate di un costruttore di copia in questi casi, anche se il costruttore ha effetti collaterali (§12.8 Copying Class Objects, Paragrafo 15).
Conclusione
Pertanto, dovremmo sempre considerare l’ottimizzazione del codice del programma utilizzando utility specializzate ove possibile, ma farlo con molta cura ed essere pronti per la probabilità di trucchi inaspettati dal compilatore a volte.
PVS-Studio
Nell’analizzatore statico PVS-Studio viene implementata una serie di diagnostica che consentono di trovare alcune situazioni in cui è possibile ottimizzare il codice. Tuttavia, PVS-Studio come qualsiasi altro analizzatore statico non può sostituire gli strumenti di profilazione. Solo gli analizzatori di programmi dinamici sono in grado di identificare i colli di bottiglia. Gli analizzatori statici non sanno quali programmi di dati di input ottengono e quanto spesso viene eseguito un particolare pezzo di codice. Ecco perché stiamo dicendo che l’analizzatore suggerisce di implementare alcune “micro-ottimizzazioni” del codice, che non garantiscono i guadagni in termini di prestazioni.
Nonostante lo svantaggio considerato, PVS-Studio analyzer funge da buon complemento agli strumenti di profilazione. Inoltre, quando si tratta di avvisi PVS-Studio, relativi all’ottimizzazione, il codice diventa spesso più semplice e più breve. Questo effetto è considerato più in dettaglio nell’articolo “Esplorare le microottimizzazioni usando il codice Tizen come esempio”.