Il presente articolo è stato tradotto automaticamente.

Compilatori

Tutto quello che ogni programmatore deve sapere sulle ottimizzazioni per i compilatori

Hadi Brais

Scaricare il codice di esempio

Linguaggi di programmazione ad alto livello offrono molti costrutti di programmazione astratte quali funzioni, istruzioni condizionali e cicli che ci rendono incredibilmente produttivo. Tuttavia, uno svantaggio di scrivere codice in un linguaggio di programmazione ad alto livello è potenzialmente significativo calo delle prestazioni. Idealmente, si dovrebbe scrivere codice comprensibile, gestibile — senza compromettere le prestazioni. Per questo motivo, i compilatori tentano di ottimizzare automaticamente il codice per migliorare le sue prestazioni, e sono diventati piuttosto sofisticati in tal modo al giorno d'oggi. Si può trasformare in cicli, istruzioni condizionali e funzioni ricorsive; eliminare interi blocchi di codice; e approfittare dell'istruzione destinazione impostata architettura (ISA) per rendere il codice più compatto e veloce. È molto meglio concentrarsi sulla scrittura di codice comprensibile, di fare delle ottimizzazioni manuali che generano codice criptico, difficile da mantenere. Infatti, manualmente, ottimizzando il codice potrebbe impedire il compilatore esegue le ottimizzazioni aggiuntive o più efficiente.

Piuttosto che manualmente ottimizzando il codice, è necessario considerare gli aspetti del vostro disegno, ad esempio utilizzando algoritmi più veloci, incorporando il parallelismo a livello di thread e utilizzando le caratteristiche specifiche del framework (ad esempio utilizzando i costruttori mossa).

Questo articolo è circa le ottimizzazioni del compilatore di Visual C++. Ho intenzione di discutere le più importanti tecniche di ottimizzazione e le decisioni di che un compilatore deve effettuare al fine di applicarle. Lo scopo non è quello di dirvi come ottimizzare manualmente il codice, ma per mostrarvi perché ci si può fidare al compilatore di ottimizzare il codice per vostro conto. Questo articolo non è affatto un esame completo delle ottimizzazioni eseguite dal compilatore Visual C++. Tuttavia, esso dimostra le ottimizzazioni che si vuole veramente sapere e come comunicare con il compilatore di applicarle.

Ci sono altre importanti ottimizzazioni che attualmente sono oltre le capacità di qualsiasi compilatore — per esempio, sostituendo un algoritmo inefficiente con una efficiente, o cambiare il layout di una struttura di dati per migliorare la sua località. Tuttavia, tali ottimizzazioni esulano dall'ambito di questo articolo.

Definendo le ottimizzazioni del compilatore

Un'ottimizzazione è il processo di trasformazione di un pezzo di codice in un altro pezzo funzionalmente equivalente del codice allo scopo di migliorare una o più delle sue caratteristiche. Le due caratteristiche più importanti sono la velocità e la dimensione del codice. Altre caratteristiche includono la quantità di energia necessaria per eseguire il codice, il tempo che necessario per compilare il codice e, nel caso in cui il codice risultante richiede la compilazione Just-in-Time (JIT), il tempo necessario a JIT compilare il codice.

Compilatori sono in costante miglioramento in termini di tecniche usano per ottimizzare il codice. Tuttavia, non sono perfetti. Eppure, invece di spendere tempo modificando manualmente un programma, è solitamente molto più proficuo per utilizzare specifiche funzionalità fornite dal compilatore e consentire al compilatore di ottimizzare il codice.

Ci sono quattro modi per aiutare il compilatore di ottimizzare il codice in modo più efficace:

  1. Scrivere codice comprensibile, gestibile. Don' t Guarda le caratteristiche orientate agli oggetti di Visual C++ come nemici di prestazioni. L'ultima versione di Visual C++ può mantenere tale sovraccarico al minimo e a volte completamente eliminarla.
  2. Utilizzare le direttive del compilatore. Per esempio, dire al compilatore di utilizzare una convenzione di chiamata è più veloce di quella di default.
  3. Utilizzare funzioni intrinseche del compilatore. Una funzione intrinseca è una funzione speciale in cui implementazione viene fornito automaticamente dal compilatore. Il compilatore ha un'intima conoscenza della funzione e sostituisce la funzione di chiamata con una sequenza estremamente efficiente delle istruzioni che sfruttano la destinazione ISA. Attualmente, Microsoft .NET Framework non supporta le funzioni intrinseche, così nessuno dei linguaggi gestiti di sostenerli. Tuttavia, Visual C++ ha ampio supporto per questa funzionalità. Si noti che mentre usando funzioni intrinseche può migliorare le prestazioni del codice, riduce la leggibilità e portabilità.
  4. Utilizzare l'ottimizzazione PGO (PGO). Con questa tecnica, il compilatore conosce più circa come il codice sta per comportarsi in fase di esecuzione e si può ottimizzare di conseguenza.

Lo scopo di questo articolo è quello di mostrare perché ci si può fidare del compilatore dimostrando le ottimizzazioni eseguite sul codice inefficiente ma comprensibile (applicando il primo metodo). Inoltre, farò una breve introduzione all'ottimizzazione PGO e citare alcune delle direttive del compilatore che consentono di perfezionare alcune parti del codice.

Ci sono molte tecniche di ottimizzazione del compilatore, che vanno da semplici trasformazioni, come costante pieghevole, a trasformazioni estreme, quali istruzioni di programmazione. Tuttavia, in questo articolo, potrai limitare la discussione di alcune delle più importanti ottimizzazioni — coloro che può significativamente migliorare le prestazioni (una percentuale a due cifre) e ridurre la dimensione di codice: funzione inline, ottimizzazioni di COMDAT e ottimizzazioni del ciclo. Io sarò discutere i primi due nella sezione successiva, poi mostrare come è possibile controllare le ottimizzazioni eseguite da Visual C++. Infine, prenderò un breve sguardo al ottimizzazioni in .NET Framework. In questo articolo, sarò con Visual Studio 2013 per compilare il codice.

Generazione di codice di tempo di collegamento (LTCG) è una tecnica per l'effettuazione di ottimizzazioni dell'intero programma (WPO) sul codice C/C++. Il compilatore C/C++ compilato separatamente ogni file di origine e produce il file oggetto corrispondente. Questo significa che il compilatore può solo applicare le ottimizzazioni su un singolo file sorgente piuttosto che su tutto il programma. Tuttavia, alcune importanti ottimizzazioni possono essere eseguite solo da guardare tutto il programma. È possibile applicare queste ottimizzazioni in fase di collegamento, piuttosto che in fase di compilazione perché il linker ha una visione completa del programma.

Quando è attivata la LTCG (specificando l'opzione del compilatore /GL), il compilatore driver (cl.exe) richiamare solo il front-end del compilatore (c1.dll o c1xx) e posticipare il lavoro di back-end (c2.dll) fino al momento del collegamento. I file risultante oggetto contengono C Inter­mediare Language (CIL) codice anziché il codice assembly dipendente dal computer. Quindi, quando viene richiamato il linker (link.exe), vede che i file oggetto contengono codice CIL e richiama il back-end del compilatore, che a sua volta esegue WPO, genera i file oggetto binario e restituisce al linker di cucire insieme tutti i file oggetto e produrre l'eseguibile.

Il front-end esegue effettivamente alcune ottimizzazioni, quali pieghevole costante, indipendentemente dal fatto se le ottimizzazioni sono abilitate o disabilitate. Tuttavia, tutte le ottimizzazioni importanti eseguite dal back-end del compilatore e possono essere controllate utilizzando le opzioni del compilatore.

LTCG consente il back-end eseguire molte ottimizzazioni aggressivamente (specificando /GL insieme il /O1 o /O2 e /Gw opzioni del compilatore e le opzioni del linker /OPT: REF e /opt: ICF). In questo articolo, illustrerò solo funzione inline e ottimizzazioni di COMDAT. Per un elenco completo delle ottimizzazioni LTCG, consultare la documentazione. Si noti che il linker può eseguire LTCG su file oggetto nativo, miscelati file oggetto nativo/gestito, puro oggetto gestito file, file oggetto gestito sicuro e sicuro netmodule.

Creerò un programma composto da due file di origine (source1.c e source2.c) e un file di intestazione (source2.h). I file source1.c e source2.c sono mostrati Figura 1 e Figura 2, rispettivamente. Il file di intestazione, che contiene i prototipi di tutte le funzioni in source2.c, è abbastanza semplice, quindi non mostrarlo qui.

Figura 1 il File source1.c

#include <stdio.h> // scanf_s and printf.
#include "Source2.h"
int square(int x) { return x*x; }
main() {
  int n = 5, m;
  scanf_s("%d", &m);
  printf("The square of %d is %d.", n, square(n));
  printf("The square of %d is %d.", m, square(m));
  printf("The cube of %d is %d.", n, cube(n));
  printf("The sum of %d is %d.", n, sum(n));
  printf("The sum of cubes of %d is %d.", n, sumOfCubes(n));
  printf("The %dth prime number is %d.", n, getPrime(n));
}

Figura 2 il File source2.c

#include <math.h> // sqrt.
#include <stdbool.h> // bool, true and false.
#include "Source2.h"
int cube(int x) { return x*x*x; }
int sum(int x) {
  int result = 0;
  for (int i = 1; i <= x; ++i) result += i;
  return result;
}
int sumOfCubes(int x) {
  int result = 0;
  for (int i = 1; i <= x; ++i) result += cube(i);
  return result;
}
static
bool isPrime(int x) {
  for (int i = 2; i <= (int)sqrt(x); ++i) {
    if (x % i == 0) return false;
  }
  return true;
}
int getPrime(int x) {
  int count = 0;
  int candidate = 2;
  while (count != x) {
    if (isPrime(candidate))
      ++count;
  }
  return candidate;
}

Il file source1.c contiene due funzioni: la funzione quadrata, che accetta un valore integer e restituisce la sua piazza, e la funzione principale del programma. La funzione principale chiama la funzione quadrata e tutte le funzioni da source2.c tranne isPrime. Il file source2.c contiene cinque funzioni: la funzione cubo restituisce il cubo di un dato numero intero; la funzione somma restituisce la somma di tutti gli interi da 1 a un dato numero intero; la funzione sumOfcubes restituisce la somma dei cubi di tutti gli interi da 1 a un dato numero intero; la funzione isPrime determina se un determinato numero intero è primo; e la funzione getPrime, che restituisce il numero x primo. Ho omesso perché non è di interesse in questo articolo di controllo degli errori.

Il codice è semplice, ma utile. Ci sono un certo numero di funzioni che eseguono calcoli semplici; alcuni richiedono semplici cicli for. La funzione getPrime è la più complessa perché contiene un po' di tempo ciclo e, all'interno del ciclo, chiama la funzione isPrime, che contiene anche un loop. Utilizzerò questo codice per dimostrare uno dei più importanti ottimizzazioni del compilatore, vale a dire la funzione inline e alcune altre ottimizzazioni.

Farò costruire il codice sotto tre diverse configurazioni ed esaminare i risultati per determinare come esso è stato trasformato dal compilatore. Se si seguono, è necessario il file di output assembler (prodotto con l'opzione del compilatore /FA [s]) per esaminare il codice assembly risultante e il file di mappa (prodotto con l'opzione del linker /MAP) per determinare le ottimizzazioni di COMDAT effettuate (linker può anche segnalare questo se si utilizza il / verbose: icf e / verbose: rif interruttori). Quindi assicuratevi di che questi interruttori sono specificati in tutte le configurazioni seguenti che discutere. Inoltre, sarò con il compilatore C (/ TC) affinché il codice generato è più facile da esaminare. Tuttavia, tutto che discutere qui vale anche per il codice C++.

La configurazione di Debug

Soprattutto perché tutte le ottimizzazioni di backend sono disabilitate quando si specifica l'opzione del compilatore /Od senza specificare l'opzione /GL, verrà utilizzata la configurazione di Debug. Quando si compila il codice in questa configurazione, i file oggetto risultante conterrà il codice binario che corrisponde esattamente al codice sorgente. È possibile esaminare i file di output assembler risultante e il file di mappa per confermare questo. Questa configurazione è equivalente per la configurazione di Debug di Visual Studio.

La configurazione di rilascio Compile-Time Code Generation

Questa configurazione è simile alla configurazione Release in cui ottimizzazioni sono attivate (specificando le opzioni di compilazione /O1, /O2 o /Ox), ma senza specificare l'opzione del compilatore /GL. In questa configurazione, l'oggetto risultante conterranno i file binari codice ottimizzato. Tuttavia, non a livello di intero programma ottimizzazioni.

Esaminando l'assembly generato listato file di source1.c, si noterà che sono state eseguite due ottimizzazioni. Primo, la prima chiamata alla piazza funzione, square(n), in Figura 1 è stata completamente eliminata valutando il calcolo in fase di compilazione. Come e ' successo? Il compilatore ha determinato che la funzione quadrata è piccola, quindi dovrebbe essere inline. Dopo l'inlining, determinato il compilatore che il valore della variabile locale n è noto e non cambia tra l'istruzione di assegnazione e la chiamata di funzione. Pertanto, essa ha concluso che è sicuro eseguire la moltiplicazione e sostituire il risultato (25). L'ottimizzazione secondo, la seconda chiamata alla funzione quadrata, square(m), è stata espansa inline, pure. Tuttavia, perché il valore di m non è noto in fase di compilazione, il compilatore non può valutare il calcolo, quindi viene emesso il codice effettivo.

Ora potrai esaminare il file di listato assembly del source2.c, che è molto più interessante. La chiamata alla funzione cubo in sumOfCubes è stata espansa inline. Questo a sua volta ha permesso al compilatore di eseguire ottimizzazioni significative sul loop (come si vedrà nella sezione "Ottimizzazioni del ciclo"). Inoltre, il set di istruzioni SSE2 sta usanda nella funzione isPrime per la conversione da int a raddoppiare quando viene chiamata la funzione sqrt e anche convertire da doppia a int quando ritornano da sqrt. E sqrt è chiamato solo una volta prima che inizi il ciclo. Si noti che se nessun /arch interruttore è specificato al compilatore, per impostazione predefinita, il compilatore 86 x utilizza SSE2. La maggior parte distribuiti x86 processori, così come tutti i processori x86-64, supportano SSE2.

La configurazione Release LTCG è identica alla configurazione Release in Visual Studio. In questa configurazione vengono attivate le ottimizzazioni e viene specificata l'opzione del compilatore /GL. Questo parametro viene specificato in modo implicito quando si utilizza /O1 o /O2. Esso indica al compilatore di generare il file oggetto CIL anziché file oggetto assieme. In questo modo, il linker richiama il back-end del compilatore per eseguire WPO come descritto in precedenza. Ora tratterò diverse ottimizzazioni WPO per mostrare l'immenso vantaggio di LTCG. I listati di codice assembly generati con questa configurazione sono disponibili online.

Fino a quando è abilitata la funzione inline (/ Ob, che viene attivata ogni volta che si richiedono ottimizzazioni), l'interruttore /GL consente al compilatore di funzioni inline definite in altre unità di traduzione indipendentemente da se è specificata l'opzione del compilatore /Gy (discusso un po' più tardi). L'opzione del linker /LTCG è opzionale e fornisce linee guida per il linker solo.

Esaminando l'Assemblea listato file di source1.c, si può vedere che tutte le chiamate di funzione tranne scanf sono stati inline. Di conseguenza, il compilatore è in grado di eseguire i calcoli del cubo, somma e sumOfCubes. Solo la funzione isPrime non è stata espansa inline. Tuttavia, se è stato inline manualmente in getPrime, il compilatore avrebbe ancora getPrime inline nel principale.

Come potete vedere, l'inlining funzione è importante non solo perché ottimizza via una chiamata di funzione, ma anche perché consente al compilatore di eseguire molte altre ottimizzazioni di conseguenza. Inline una funzione solitamente migliora le prestazioni a scapito di aumentare le dimensioni del codice. L'uso eccessivo di questa ottimizzazione conduce ad un fenomeno conosciuto come codice di gonfiare. Ogni sito di chiamata, il compilatore esegue un'analisi costi/benefici e poi decide se inline la funzione.

A causa dell'importanza di inline, il compilatore Visual C++ fornisce il supporto molto di più rispetto a ciò che detta lo standard riguardanti l'inlining controllo. Si può dire al compilatore di mai in una gamma di funzioni di linea utilizzando il pragma auto_inline. Si può dire al compilatore di mai inline una funzione specifica o un metodo contrassegnandola con __declspec(noinline). È possibile contrassegnare una funzione con la parola chiave inline per dare un suggerimento per il compilatore in linea la funzione (anche se il compilatore può scegliere di ignorare questo Suggerimento Se l'inlining sarebbe una perdita netta). La parola chiave inline è stato disponibile fin dalla prima versione di C++ — è stato introdotto nel C99. È possibile utilizzare la parola chiave specifiche Microsoft inline nel codice C e in C++; è utile quando stai usando una vecchia versione di C che non supporta questa parola chiave. Inoltre, è possibile utilizzare la parola chiave forceinline (C e C++) per forzare il compilatore a sempre inline una funzione quando possibile. E ultimo ma non meno importante, il compilatore a svolgersi una funzione ricorsiva si può dire ad una profondità indefinita o specifica di inline utilizzando il pragma inline_recursion. Si noti che il compilatore non offre attualmente nessuna funzionalità che consentono al controllo l'inlining presso il sito di chiamata, piuttosto che alla definizione della funzione.

Il /Ob0 interruttore Disabilita l'inlining completamente, che prende effetto per impostazione predefinita. È necessario utilizzare questa opzione quando il debug (automaticamente è specificato nella configurazione di Debug Visual Studio ). L'interruttore /Ob1 indica al compilatore di considerare solo le funzioni per l'inlining che sono contrassegnati con inline, inline o forceinline. L'interruttore di /Ob2, che prende effetto quando si specifica /O [1|2|x], indica al compilatore di considerare qualsiasi funzione per l'inlining. A mio parere, l'unico motivo per utilizzare le parole chiave inline o inline è a controllo inline con l'interruttore /Ob1.

Il compilatore non sarà in grado di inline una funzione in determinate condizioni. Un esempio è quando viene chiamata una funzione virtuale praticamente; la funzione non può essere inline perché il compilatore non può sapere quale funzione sta per essere chiamato. Un altro esempio è quando viene chiamata una funzione tramite un puntatore a funzione, piuttosto che usando il suo nome. Si dovrebbe cercare di evitare tali condizioni per consentire l'inlining. Consultare la documentazione MSDN per un elenco completo di tali condizioni.

Funzione inline non è l'unica ottimizzazione che è più effec­tive quando applicato a livello di tutto il programma. Infatti, la maggior parte delle ottimizzazioni diventano più efficaci a quel livello. Nel resto di questa sezione, mi occuperò di una classe specifica di tali ottimizzazioni chiamato ottimizzazioni di COMDAT.

Per impostazione predefinita, quando si compila un'unità di traduzione, tutto il codice sarà essere memorizzato in una singola sezione nel file oggetto. Il linker opera a livello di sezione. Cioè, può rimuovere sezioni, combinare sezioni e riordinare le sezioni. Questo preclude il linker di eseguire tre ottimizzazioni che può significativamente (percentuale a due cifre) ridurre le dimensioni dell'eseguibile e migliorarne le prestazioni. Il primo è eliminando senza riferimenti funzioni e variabili globali. Il secondo è pieghevole identiche funzioni e variabili globali costante. Il terzo è riordinamento delle funzioni e variabili globali così quelle funzioni che cadono su stesso percorso di esecuzione e quelle variabili che sono accessibili insieme si trovano fisicamente più vicini in memoria per migliorare la località.

Per attivare queste ottimizzazioni del linker, si può dire al compilatore di pacchetto funzioni e variabili in sezioni separate specificando il /Gy (collegamento a livello di funzione) e le opzioni del compilatore /Gw (ottimizzazione di dati globali), rispettivamente. Tali sezioni sono chiamati COMDAT. È anche possibile contrassegnare una variabile particolare dati globali con declspec (selectany) per indicare al compilatore per imballare la variabile in un COMDAT. Quindi, specificando l'opzione del linker /OPT: REF, il linker eliminerà senza riferimenti funzioni e variabili globali. Inoltre, specificando l'opzione /OPT: ICF, il linker si piega identiche funzioni e variabili globali costante. (ICF acronimo di COMDAT identici pieghevole). Con l'opzione del linker /ORDER, è possibile indicare al linker di collocare COMDAT nell'immagine risultante in un ordine specifico. Si noti che tutte queste ottimizzazioni sono ottimizzazioni del linker e non richiedono l'opzione del compilatore /GL. /OPT: REF e /opt: ICF interruttori devono essere disabilitati durante il debug per ovvi motivi.

È consigliabile utilizzare LTCG quando possibile. L'unico motivo per non utilizzare LTCG è quando si desidera distribuire i file di libreria e oggetto risultante. È importante ricordare che questi file contengono CIL codice anziché il codice assembly. Codice CIL può essere consumati solo dal compilatore/linker della stessa versione che l'ha prodotta, che può limitare in misura significativa l'usabilità del file oggetto, perché gli sviluppatori devono avere la stessa versione del compilatore di utilizzare questi file. In questo caso, se non sei disposto a distribuire i file oggetto per ogni versione del compilatore, si dovrebbe utilizzare generazione codice compile-time. Oltre alla limitata usabilità, questi file oggetto sono molte volte in formato più grandi del corrispondente file oggetto assembler. Tuttavia, tenere presente l'enorme vantaggio di file oggetto CIL, che consente ai WPO.

Ottimizzazioni del ciclo

Il compilatore Visual C++ supporta diverse ottimizzazioni del ciclo, ma mi occuperò solo tre: vettorizzazione automatica, svolgimento ciclo e moto codice invariante di ciclo. Se si modifica il codice in Figura 1 modo che m è passato a sumOfCubes invece di n, il compilatore non sarà in grado di determinare il valore del parametro, quindi è necessario compilare la funzione per gestire qualsiasi argomento. La funzione risultante è altamente ottimizzata e la sua dimensione è piuttosto grande, quindi il compilatore non in linea si.

Compilazione del codice con i risultati di interruttore /O1 in codice assembly che è ottimizzato per lo spazio. In questo caso, nessun ottimizzazioni verranno eseguite la funzione sumOfCubes. Compilazione con i risultati di interruttore /O2 nel codice che viene ottimizzato per la velocità. La dimensione del codice sarà significativamente più grande ancora significativamente più veloce perché il ciclo all'interno di sumOfCubes è stato srotolato e vettorizzato. È importante capire che vettorizzazione non sarebbe stato possibile senza l'inlining funzione cubo. Inoltre, srotolamento del ciclo non sarebbe così efficace senza l'inlining. Una rappresentazione grafica semplificata del codice assembly risultante è mostrata Figura 3. Il grafico del flusso è lo stesso per le architetture sia x86 e x86-64.

controllo flusso grafico di sumOfCubes
Figura 3 controllo flusso grafico di sumOfCubes

In Figura 3, il diamante verde è il punto di ingresso e i rettangoli in rossi sono i punti di uscita. I diamanti blu rappresentano condizioni che vengono eseguite come parte della funzione sumOfCubes in fase di esecuzione. Se SSE4 è supportata dal processore e x è maggiore o uguale a otto, poi istruzioni SSE4 utilizzerà per eseguire quattro moltiplicazioni allo stesso tempo. Il processo di esecuzione la stessa operazione su più valori contemporaneamente è chiamato vettorizzazione. Inoltre, il compilatore sarà srotolare il ciclo due volte; cioè, il corpo del ciclo verrà ripetuto due volte in ogni iterazione. L'effetto combinato è che otto moltiplicazioni verranno effettuate per ogni iterazione. Quando x diventa meno di otto, istruzioni tradizionale verranno utilizzate per eseguire il resto dei calcoli. Si noti che il compilatore ha generato tre punti di uscita contenente gli epiloghi separati nella funzione invece di uno solo. Questo riduce il numero di salti.

Srotolamento del loop è il processo di ripetizione corpo del ciclo all'interno del ciclo, così che più di una iterazione del ciclo viene eseguita all'interno di una singola iterazione del ciclo srotolato. La ragione di che questo migliora le prestazioni è che le istruzioni di controllo del ciclo saranno eseguite meno frequentemente. Forse più importante, esso potrebbe consentire al compilatore di eseguire molte altre ottimizzazioni, quali vettorizzazione. Il downside di srotolamento è che aumenta la dimensione del codice e registrare la pressione. Tuttavia, a seconda del corpo del ciclo, potrebbe migliorare le prestazioni di una percentuale a due cifre.

A differenza di x86 processori, supporto di processori x86-64 tutti SSE2. Inoltre, è possibile sfruttare i set di istruzioni AVX/AVX2 delle ultime microarchitetture x86-64 di Intel e AMD specificando il /arch passare. Specificando /arch:AVX2 consente al compilatore di utilizzare le FMA e BMI set di istruzioni, pure.

Attualmente, il compilatore Visual C++ non consentirà di srotolamento del loop di controllo. Tuttavia, è possibile emulare questa tecnica utilizzando modelli insieme con la parola chiave di _ forceinline. È possibile disabilitare auto-vettorizzazione su un ciclo specifico utilizzando il pragma ciclo con l'opzione no_vector.

Guardando il codice assembly generato, occhi appassionati noterebbe che il codice può essere ottimizzato un po' di più. Tuttavia, il compilatore ha già fatto un grande lavoro e non spendono molto più tempo di analisi del codice e l'applicazione di ottimizzazioni minori.

someOfCubes non è l'unica funzione cui ciclo è stato srotolato. Se si modifica il codice così che m viene passato alla funzione somma invece di n, il compilatore non sarà in grado di valutare la funzione e, pertanto, ha generare il relativo codice. In questo caso, il ciclo sarà srotolato due volte.

L'ultima ottimizzazione che tratterò è movimento codice invariante di ciclo. Si consideri il seguente pezzo di codice:

int sum(int x) {
  int result = 0;
  int count = 0;
  for (int i = 1; i <= x; ++i) {
    ++count;
    result += i;
  }
  printf("%d", count);
  return result;
}

Qui l'unico cambiamento è che ho un'altra variabile che viene incrementato in ogni iterazione e poi stampato. Non è difficile vedere che questo codice può essere ottimizzato spostando l'incremento della variabile count all'esterno del ciclo. È appena posso assegnare x per la variabile count. Questa ottimizzazione è chiamata moto codice invariante di ciclo. La parte invariante di ciclo indica chiaramente che questa tecnica funziona solo quando il codice non dipende le espressioni nell'intestazione del ciclo.

Ora ecco l'inghippo: Se si applica questa ottimizzazione manualmente, il codice risultante potrebbe esibire prestazioni degradate in determinate condizioni. Si può capire perché? Si consideri cosa succede quando x è nonpositive. Il ciclo viene eseguito mai, il che significa che nella versione non ottimizzata, il variabile conteggio non essere toccato. Come­mai, nella versione ottimizzata manualmente, un'assegnazione inutile da x a contare viene eseguita all'esterno del ciclo! Inoltre, se x è negativo, conteggio tenere il valore sbagliato. Compilatori e gli esseri umani sono soggetti a tali insidie. Fortunatamente, il compilatore Visual C++ è abbastanza intelligente per realizzare questo emettendo la condizione del ciclo prima dell'assegnazione, determinando un miglioramento delle prestazioni per tutti i valori di x.

In sintesi, se sei un compilatore né un esperto, le ottimizzazioni del compilatore si dovrebbe evitare di fare trasformazioni manuale al codice solo per farlo sembrare più velocemente. Mantenere le mani pulite e fiducia il compilatore per ottimizzare il codice.

Controllando le ottimizzazioni

Oltre al compilatore interruttori /O1, /O2 e /Ox, è possibile controllare le ottimizzazioni per funzioni specifiche utilizzando il pragma optimize, che assomiglia a questo:

#pragma optimize( "[optimization-list]", {on | off} )

Elenco di ottimizzazione può essere vuoto o contenere uno o più dei seguenti valori: g, s, t e y. Questi corrispondono agli interruttori del compilatore /Og, /Os, /Ot e /Oy, rispettivamente.

Una lista vuota con il parametro off provoca tutte queste ottimizzazioni per essere spento indipendentemente le opzioni del compilatore che sono state specificate. Una lista vuota con il parametro in causa le opzioni del compilatore specificato effettive.

L'interruttore /Og Attiva le ottimizzazioni globali, che sono quelli che possono essere eseguite osservando la funzione essendo ottimizzata solo, non in una qualsiasi delle funzioni che chiama. Se è abilitato LTCG, /Og Attiva WPO.

Il pragma optimize è utile quando si desidera differenti funzioni ottimizzate in diversi modi — alcuni per lo spazio e gli altri per velocità. Tuttavia, se si vuole veramente avere quel livello di controllo, è opportuno ottimizzazione PGO (PGO), che è il processo di ottimizzazione del codice utilizzando un profilo che contiene informazioni comportamentali registrate durante l'esecuzione di una versione instrumentata del codice. Il compilatore utilizza il profilo per prendere decisioni migliori su come ottimizzare il codice. Visual Studio fornisce gli strumenti necessari per applicare questa tecnica su codice nativo e gestito.

Ottimizzazioni in .NET

Non non c'è nessun linker coinvolti nel modello di compilazione .NET. Tuttavia, c'è un compilatore di codice sorgente (compilatore C#) e un compilatore JIT. Il compilatore di codice sorgente esegue solo le ottimizzazioni minori. Ad esempio, non eseguire l'inline di funzioni e ottimizzazioni del ciclo. Invece, queste ottimizzazioni sono gestite dal compilatore JIT. Il compilatore JIT fornito con tutte le versioni di .NET Framework fino a 4.5 non supporta le istruzioni SIMD. Tuttavia, il compilatore JIT fornito con .NET Framework 4.5.1 e versioni successive, chiamate RyuJIT, supporti SIMD.

Qual è la differenza tra RyuJIT e Visual C++ in termini di capacità di ottimizzazione? Perché fa il suo lavoro in fase di esecuzione, RyuJIT può eseguire ottimizzazioni che Visual C++ non può. Ad esempio, in fase di esecuzione, RyuJIT potrebbe essere in grado di determinare che la condizione di un se istruzione non è mai vero in questa particolare esecuzione dell'applicazione e, pertanto, può essere ottimizzato via. Anche RyuJIT può sfruttare le funzionalità del processore su cui è in esecuzione. Ad esempio, se il processore supporta offrendo, il compilatore JIT emetterà solo offrendo istruzioni per la funzione sumOfcubes, rendendo molto più compatto del codice generato. Tuttavia, esso non può spendere molto tempo ottimizzando il codice perché il tempo impiegato per la compilazione JIT urta le prestazioni dell'applicazione. D'altra parte, il compilatore Visual C++ può spendere molto più tempo per individuare altri oppor ottimizzazione­tunities e approfitta di loro. Una grande nuova tecnologia di Microsoft, chiamato .NET nativo, consente di compilare codice gestito in eseguibili indipendenti ottimizzati utilizzando il Visual C++ indietro fine. Attualmente, questa tecnologia supporta solo applicazioni Windows Store.

La capacità di controllare le ottimizzazioni del codice gestito è attualmente limitata. I compilatori c# e Visual Basic forniscono solo la possibilità di attivare o disattivare le ottimizzazioni utilizzando il / ottimizzare l'interruttore. Per controllare le ottimizzazioni JIT, è possibile applicare il System.Runtime.Compiler­Services.MethodImpl attributo su un metodo con un'opzione da MethodImplOptions specificato. L'opzione NoOptimization si spegne le ottimizzazioni, l'opzione NoInlining impedisce il metodo da essere inline e l'opzione AggressiveInlining (.NET 4.5) dà una raccomandazione (più di un pizzico) di al compilatore JIT di inline il metodo.

Il confezionamento

Tutte le tecniche di ottimizzazione discussione in questo articolo può migliorare significativamente le prestazioni del vostro codice di una percentuale a due cifre, e tutti loro sono supportati dal compilatore Visual C++. Ciò che rende queste tecniche importanti è che, quando applicati, consentono al compilatore di eseguire altre ottimizzazioni. Questo non è affatto una trattazione completa delle ottimizzazioni del compilatore di Visual C++. Tuttavia, spero che ti ha dato un apprezzamento per le funzionalità del compilatore. Visual C++ può fare di più, molto di più, quindi rimanete sintonizzati per la parte 2.


Hadi Brais è un pH.d. studioso presso l'Istituto indiano di tecnologia di Delhi (IITD), ricercando le ottimizzazioni del compilatore per la tecnologia di prossima generazione di memoria. Egli trascorre la maggior parte del suo tempo a scrivere codice in C / C + + / C# e scavando in profondità il CLR e CRT. Ha Blog a hadibrais.wordpress.com. Contattarlo al hadi.b@live.com.

Grazie al seguente Microsoft esperto tecnico per la revisione di questo articolo: Jim Hogg