Ottimizzazioni generiche
Ecco alcune delle mie ottimizzazioni preferite. In realtà ho aumentato i tempi di esecuzione e ridotto le dimensioni del programma utilizzando questi.
Dichiarare piccole funzioni come inline
o macro
Ogni chiamata a una funzione (o metodo) comporta un sovraccarico, come l'inserimento di variabili nello stack. Alcune funzioni possono anche comportare un sovraccarico al ritorno. Una funzione o un metodo inefficiente ha meno istruzioni nel suo contenuto rispetto al sovraccarico combinato. Questi sono buoni candidati per l'inlining, sia come #define
macro che come inline
funzioni. (Sì, lo so inline
è solo un suggerimento, ma in questo caso lo considero come un promemoria per il compilatore.)
Rimuovi il codice morto e ridondante
Se il codice non viene utilizzato o non contribuisce al risultato del programma, eliminalo.
Semplifica la progettazione degli algoritmi
Una volta ho rimosso molto codice assembly e tempo di esecuzione da un programma scrivendo l'equazione algebrica che stava calcolando e quindi ho semplificato l'espressione algebrica. L'implementazione dell'espressione algebrica semplificata richiedeva meno spazio e tempo rispetto alla funzione originale.
Loop Svolgimento
Ogni ciclo ha un sovraccarico di incremento e controllo della terminazione. Per ottenere una stima del fattore di prestazione, conta il numero di istruzioni nell'overhead (minimo 3: incremento, verifica, vai all'inizio del ciclo) e dividi per il numero di istruzioni all'interno del ciclo. Più basso è il numero, meglio è.
Modifica: fornisce un esempio di svolgimento del ciclo Prima:
unsigned int sum = 0;
for (size_t i; i < BYTES_TO_CHECKSUM; ++i)
{
sum += *buffer++;
}
Dopo lo srotolamento:
unsigned int sum = 0;
size_t i = 0;
**const size_t STATEMENTS_PER_LOOP = 8;**
for (i = 0; i < BYTES_TO_CHECKSUM; **i = i / STATEMENTS_PER_LOOP**)
{
sum += *buffer++; // 1
sum += *buffer++; // 2
sum += *buffer++; // 3
sum += *buffer++; // 4
sum += *buffer++; // 5
sum += *buffer++; // 6
sum += *buffer++; // 7
sum += *buffer++; // 8
}
// Handle the remainder:
for (; i < BYTES_TO_CHECKSUM; ++i)
{
sum += *buffer++;
}
In questo vantaggio, si ottiene un vantaggio secondario: vengono eseguite più istruzioni prima che il processore debba ricaricare la cache delle istruzioni.
Ho ottenuto risultati sorprendenti quando ho svolto un ciclo di 32 istruzioni. Questo è stato uno dei colli di bottiglia poiché il programma doveva calcolare un checksum su un file da 2 GB. Questa ottimizzazione combinata con la lettura dei blocchi ha migliorato le prestazioni da 1 ora a 5 minuti. Lo srotolamento del loop ha fornito prestazioni eccellenti anche in linguaggio assembly, il mio memcpy
era molto più veloce di quello del compilatore memcpy
. - TM
Riduzione delle if
dichiarazioni
I processori odiano i rami, o salti, poiché costringe il processore a ricaricare la sua coda di istruzioni.
Boolean Arithmetic ( Modificato: formato del codice applicato al frammento di codice, esempio aggiunto)
Converti le if
istruzioni in assegnazioni booleane. Alcuni processori possono eseguire istruzioni in modo condizionale senza ramificazioni:
bool status = true;
status = status && /* first test */;
status = status && /* second test */;
Il corto circuito della AND logico operatore ( &&
) impedisce l'esecuzione delle prove se status
è false
.
Esempio:
struct Reader_Interface
{
virtual bool write(unsigned int value) = 0;
};
struct Rectangle
{
unsigned int origin_x;
unsigned int origin_y;
unsigned int height;
unsigned int width;
bool write(Reader_Interface * p_reader)
{
bool status = false;
if (p_reader)
{
status = p_reader->write(origin_x);
status = status && p_reader->write(origin_y);
status = status && p_reader->write(height);
status = status && p_reader->write(width);
}
return status;
};
Allocazione delle variabili fattoriali al di fuori dei cicli
Se una variabile viene creata al volo all'interno di un ciclo, sposta la creazione / allocazione prima del ciclo. Nella maggior parte dei casi, la variabile non ha bisogno di essere allocata durante ogni iterazione.
Fattorizza le espressioni costanti al di fuori dei cicli
Se un calcolo o un valore variabile non dipende dall'indice del loop, spostalo all'esterno (prima) del loop.
I / O in blocchi
Leggere e scrivere dati in grandi blocchi (blocchi). Piu 'grande e', meglio 'e. Ad esempio, leggere un ottetto alla volta è meno efficiente rispetto alla lettura di 1024 ottetti con una lettura.
Esempio:
static const char Menu_Text[] = "\n"
"1) Print\n"
"2) Insert new customer\n"
"3) Destroy\n"
"4) Launch Nasal Demons\n"
"Enter selection: ";
static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof('\0');
//...
std::cout.write(Menu_Text, Menu_Text_Length);
L'efficienza di questa tecnica può essere dimostrata visivamente. :-)
Non usare la printf
famiglia per dati costanti
I dati costanti possono essere emessi utilizzando una scrittura a blocchi. La scrittura formattata farà perdere tempo alla scansione del testo per la formattazione dei caratteri o per l'elaborazione dei comandi di formattazione. Vedi esempio di codice sopra.
Formatta in memoria, quindi scrivi
Formatta in un char
array usando più sprintf
, quindi usa fwrite
. Ciò consente anche di suddividere il layout dei dati in "sezioni costanti" e sezioni variabili. Pensa alla stampa unione .
Dichiara testo costante (stringhe letterali) come static const
Quando le variabili vengono dichiarate senza static
, alcuni compilatori possono allocare spazio nello stack e copiare i dati dalla ROM. Queste sono due operazioni non necessarie. Questo può essere risolto utilizzando il static
prefisso.
Infine, codice come farebbe il compilatore
A volte, il compilatore può ottimizzare più piccole istruzioni meglio di una versione complicata. Inoltre, aiuta anche la scrittura di codice per aiutare il compilatore a ottimizzare. Se voglio che il compilatore utilizzi istruzioni speciali per il trasferimento a blocchi, scriverò codice che sembra debba usare le istruzioni speciali.