Pratiche di codifica che consentono al compilatore / ottimizzatore di realizzare un programma più veloce


116

Molti anni fa, i compilatori C non erano particolarmente intelligenti. Per aggirare il problema K&R ha inventato la parola chiave register , per suggerire al compilatore, che forse sarebbe una buona idea mantenere questa variabile in un registro interno. Hanno anche creato l'operatore terziario per aiutare a generare codice migliore.

Col passare del tempo, i compilatori sono maturati. Sono diventati molto intelligenti in quanto la loro analisi del flusso consente loro di prendere decisioni migliori su quali valori tenere nei registri di quanto tu possa fare. La parola chiave del registro è diventata irrilevante.

FORTRAN può essere più veloce di C per alcuni tipi di operazioni, a causa di problemi di alias . In teoria con un'attenta codifica, è possibile aggirare questa restrizione per consentire all'ottimizzatore di generare codice più veloce.

Quali pratiche di codifica sono disponibili che possono consentire al compilatore / ottimizzatore di generare codice più veloce?

  • Sarebbe gradito identificare la piattaforma e il compilatore che utilizzi.
  • Perché la tecnica sembra funzionare?
  • Il codice di esempio è incoraggiato.

Ecco una domanda correlata

[Modifica] Questa domanda non riguarda il processo generale da profilare e ottimizzare. Supponiamo che il programma sia stato scritto correttamente, compilato con piena ottimizzazione, testato e messo in produzione. Potrebbero esserci costrutti nel codice che impediscono all'ottimizzatore di fare il miglior lavoro possibile. Cosa puoi fare per eseguire il refactoring che rimuoverà questi divieti e consentirà all'ottimizzatore di generare codice ancora più veloce?

[Modifica] Link correlato offset


7
Potrebbe essere un buon candidato per la community wiki imho poiché non esiste una risposta definitiva `` singola '' a questa domanda (interessante) ...
ChristopheD

Mi manca ogni volta. Grazie per averlo precisato.
EvilTeach

Con "migliore" intendi semplicemente "più veloce" o hai in mente altri criteri di eccellenza?
High Performance Mark

1
È piuttosto difficile scrivere un buon allocatore di registri, soprattutto in modo portatile, e l'allocazione dei registri è assolutamente essenziale per le prestazioni e la dimensione del codice. registereffettivamente reso il codice sensibile alle prestazioni più portabile combattendo i compilatori scadenti.
Potatoswatter

1
@EvilTeach: community wiki non significa "nessuna risposta definitiva", non è sinonimo di tag soggettivo. Wiki della comunità significa che vuoi cedere il tuo post alla comunità in modo che altre persone possano modificarlo. Non sentirti obbligato a wiki le tue domande se non ne hai voglia.
Juliet

Risposte:


54

Scrive su variabili locali e non restituisce argomenti! Questo può essere di grande aiuto per aggirare i rallentamenti dell'aliasing. Ad esempio, se il tuo codice ha l'aspetto

void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    for (int i=0; i<numFoo, i++)
    {
         barOut.munge(foo1, foo2[i]);
    }
}

il compilatore non sa che foo1! = barOut, e quindi deve ricaricare foo1 ogni volta attraverso il ciclo. Inoltre non può leggere foo2 [i] finché la scrittura su barOut non è terminata. Potresti iniziare a scherzare con i puntatori limitati, ma è altrettanto efficace (e molto più chiaro) farlo:

void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    Foo barTemp = barOut;
    for (int i=0; i<numFoo, i++)
    {
         barTemp.munge(foo1, foo2[i]);
    }
    barOut = barTemp;
}

Sembra sciocco, ma il compilatore può essere molto più intelligente nel trattare con la variabile locale, dal momento che non può sovrapporsi in memoria con nessuno degli argomenti. Questo può aiutarti a evitare il temuto load-hit-store (menzionato da Francis Boivin in questo thread).


7
Questo ha l'ulteriore vantaggio di rendere le cose più facili da leggere / capire anche per i programmatori, dal momento che non devono preoccuparsi nemmeno di possibili effetti collaterali non ovvi.
Michael Burr

La maggior parte degli IDE visualizza le variabili locali per impostazione predefinita, quindi c'è meno digitazione
EvilTeach

9
puoi anche abilitare tale ottimizzazione utilizzando puntatori limitati
Ben Voigt

4
@ Ben - è vero, ma penso che in questo modo sia più chiaro. Inoltre, se l'input e l'output si sovrappongono, credo che il risultato non sia specificato con puntatori limitati (probabilmente otterrà un comportamento diverso tra debug e rilascio), mentre in questo modo sarà almeno coerente. Non fraintendetemi, mi piace usare la limitazione, ma non ne ho più bisogno.
celion

Devi solo sperare che Foo non abbia un'operazione di copia definita che copia un paio di mega di dati ;-)
Skizz

76

Ecco una pratica di codifica per aiutare il compilatore a creare codice veloce: qualsiasi linguaggio, qualsiasi piattaforma, qualsiasi compilatore, qualsiasi problema:

Do Non utilizzare trucchi intelligenti quali la forza, o addirittura incoraggiano, il compilatore per gettare le variabili in memoria (tra cui cache e registri) come si pensa meglio. Prima scrivi un programma che sia corretto e gestibile.

Quindi, profila il tuo codice.

Quindi, e solo allora, potresti voler iniziare a studiare gli effetti del dire al compilatore come usare la memoria. Apporta 1 modifica alla volta e misura il suo impatto.

Aspettati di essere deluso e di dover lavorare davvero molto duramente per piccoli miglioramenti delle prestazioni. I compilatori moderni per linguaggi maturi come Fortran e C sono molto, molto buoni. Se leggi un resoconto di un "trucco" per ottenere prestazioni migliori dal codice, tieni presente che anche gli autori del compilatore lo hanno letto e, se vale la pena farlo, probabilmente lo hanno implementato. Probabilmente hanno scritto quello che hai letto in primo luogo.


20
Gli sviluppatori più affidabili hanno un tempo limitato, proprio come tutti gli altri. Non tutte le ottimizzazioni verranno inserite nel compilatore. Come &vs. %per potenze di due (raramente, se non mai, ottimizzato, ma può avere impatti significativi sulle prestazioni). Se leggi un trucco per le prestazioni, l'unico modo per sapere se funziona è apportare il cambiamento e misurare l'impatto. Non dare mai per scontato che il compilatore ottimizzi qualcosa per te.
Dave Jarvis,

22
& e% è praticamente sempre ottimizzato, insieme alla maggior parte degli altri trucchi aritmetici economici come gratuiti. Ciò che non viene ottimizzato è il caso in cui l'operando di destra è una variabile che sembra essere sempre una potenza di due.
Potatoswatter

8
Per chiarire, mi sembra di aver confuso alcuni lettori: il consiglio nella pratica di codifica che propongo è di sviluppare prima un codice semplice che non faccia uso di istruzioni di layout di memoria per stabilire una linea di base delle prestazioni. Quindi, prova le cose una alla volta e misura il loro impatto. Non ho offerto alcun consiglio sull'esecuzione delle operazioni.
High Performance Mark

17
Per una potenza di due costante n, gcc sostituisce % ncon & (n-1) anche quando l'ottimizzazione è disabilitata . Non è esattamente "raramente, se non mai" ...
Porculus

12
% NON PU essere ottimizzato come & quando il tipo è firmato, a causa delle regole idiote di C per la divisione intera negativa (arrotondare verso 0 e avere resto negativo, invece di arrotondare per difetto e avere sempre resto positivo). E la maggior parte delle volte, i programmatori ignoranti usano tipi con segno ...
R .. GitHub STOP HELPING ICE

47

L'ordine in cui attraversi la memoria può avere un impatto profondo sulle prestazioni ei compilatori non sono molto bravi a capirlo e risolverlo. Devi essere coscienzioso delle preoccupazioni relative alla località della cache quando scrivi codice se ti interessano le prestazioni. Ad esempio, gli array bidimensionali in C sono allocati nel formato di riga principale. Attraversare gli array nel formato della colonna principale tenderà a farti avere più cache mancate e rendere il tuo programma più limitato alla memoria rispetto al processore:

#define N 1000000;
int matrix[N][N] = { ... };

//awesomely fast
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[i][j];
  }
}

//painfully slow
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[j][i];
  }
}

A rigor di termini, questo non è un problema di ottimizzazione, ma è un problema di ottimizzazione.
EvilTeach

10
Sicuramente è un problema di ottimizzazione. Le persone scrivono da decenni articoli sull'ottimizzazione dell'interscambio di loop automatico.
Phil Miller

20
@ Potatoswatter Di cosa stai parlando? Il compilatore C può fare tutto ciò che vuole purché si osservi lo stesso risultato finale, e in effetti GCC 4.4 ha -floop-interchangeche invertirà un ciclo interno ed esterno se l'ottimizzatore lo ritiene redditizio.
effimero

2
Eh, beh, eccoti. La semantica C è spesso rovinata da problemi di aliasing. Immagino che il vero consiglio qui sia di passare quella bandiera!
Potatoswatter

36

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 inlineo 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 #definemacro che come inlinefunzioni. (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 memcpyera molto più veloce di quello del compilatore memcpy. - TM

Riduzione delle ifdichiarazioni

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 ifistruzioni 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 chararray 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 staticprefisso.

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.


2
Interessante puoi fornire un esempio in cui hai ottenuto un codice migliore con alcune piccole istruzioni, invece di una più grande. Puoi mostrare un esempio di riscrittura di un if, usando booleani. In generale, lascerei il ciclo srotolato al compilatore, poiché probabilmente ha una sensazione migliore per la dimensione della cache. Sono un po 'sorpreso dall'idea di sprintfing, poi fwriting. Penso che fprintf lo faccia effettivamente sotto il cofano. Puoi fornire qualche dettaglio in più qui?
EvilTeach

1
Non vi è alcuna garanzia che la fprintfformattazione su un buffer separato restituisca il buffer. Un semplificato (per l'uso della memoria) fprintfprodurrebbe tutto il testo non formattato, quindi formatterà e emetterà e ripeterà fino a quando l'intera stringa di formato non viene elaborata, effettuando così 1 chiamata di output per ogni tipo di output (formattato o non formattato). Altre implementazioni avrebbero bisogno di allocare dinamicamente la memoria per ogni chiamata per contenere l'intera nuova stringa (che è un male nell'ambiente dei sistemi embedded). Il mio suggerimento riduce il numero di uscite.
Thomas Matthews

3
Una volta ho ottenuto un miglioramento significativo delle prestazioni arrotolando un loop. Quindi ho capito come arrotolarlo più stretto usando qualche indiretto, e il programma è diventato notevolmente più veloce. (La creazione di profili ha mostrato che questa particolare funzione rappresenta il 60-80% del tempo di esecuzione e ho testato attentamente le prestazioni prima e dopo.) Credo che il miglioramento fosse dovuto a una località migliore, ma non ne sono completamente sicuro.
David Thornley

16
Molte di queste sono ottimizzazioni del programmatore piuttosto che modi per i programmatori di aiutare il compilatore all'ottimizzazione, che era il fulcro della domanda originale. Ad esempio, loop srotolamento. Sì, puoi eseguire da solo lo srotolamento, ma penso che sia più interessante capire quali ostacoli ci sono per il compilatore che si srotola per te e li rimuove.
Adrian McCarthy

26

L'ottimizzatore non ha davvero il controllo delle prestazioni del tuo programma, lo sei. Utilizzare algoritmi e strutture appropriati e profilo, profilo, profilo.

Detto questo, non dovresti eseguire il ciclo interno su una piccola funzione da un file in un altro file, poiché ciò impedisce che venga inline.

Evita di prendere l'indirizzo di una variabile, se possibile. Chiedere un puntatore non è "libero" in quanto significa che la variabile deve essere tenuta in memoria. Anche un array può essere mantenuto nei registri se si evitano i puntatori: questo è essenziale per la vettorializzazione.

Il che porta al punto successivo, leggi il manuale ^ # $ @ ! GCC può vettorializzare il codice C semplice se spargi un __restrict__qui e un __attribute__( __aligned__ )là. Se vuoi qualcosa di molto specifico dall'ottimizzatore, potresti dover essere specifico.


14
Questa è una buona risposta, ma tieni presente che l'ottimizzazione dell'intero programma sta diventando sempre più popolare e può infatti integrare le funzioni tra le unità di traduzione.
Phil Miller

1
@Novelocrat Sì - inutile dire che sono rimasto molto sorpreso la prima volta che ho visto qualcosa da A.cinserire in B.c.
Jonathon Reinhart

18

Sulla maggior parte dei processori moderni, il collo di bottiglia più grande è la memoria.

Aliasing: Load-Hit-Store può essere devastante in un ciclo ristretto. Se stai leggendo una posizione di memoria e scrivendo in un'altra e sai che sono disgiunte, mettere con attenzione una parola chiave alias sui parametri della funzione può davvero aiutare il compilatore a generare codice più veloce. Tuttavia, se le regioni di memoria si sovrappongono e hai usato "alias", sei pronto per una buona sessione di debug di comportamenti indefiniti!

Cache-miss: non sono proprio sicuro di come si possa aiutare il compilatore poiché è principalmente algoritmico, ma ci sono elementi intrinseci per il precaricamento della memoria.

Inoltre, non provare a convertire i valori in virgola mobile in int e viceversa troppo poiché usano registri diversi e convertire da un tipo a un altro significa chiamare l'istruzione di conversione effettiva, scrivere il valore in memoria e leggerlo nuovamente nel set di registri appropriato .


4
+1 per load-hit-store e diversi tipi di registro. Non sono sicuro di quanto sia grande un affare in x86, ma stanno devastando PowerPC (ad esempio Xbox360 e Playstation3).
celion

La maggior parte dei documenti sulle tecniche di ottimizzazione del ciclo del compilatore presuppone una nidificazione perfetta, il che significa che il corpo di ogni ciclo tranne il più interno è solo un altro ciclo. Questi documenti semplicemente non discutono i passaggi necessari per generalizzarli, anche se è molto chiaro che possono esserlo. Pertanto, mi aspetto che molte implementazioni non supportino effettivamente quelle generalizzazioni, a causa dello sforzo aggiuntivo richiesto. Pertanto, i molti algoritmi per ottimizzare l'utilizzo della cache nei loop potrebbero funzionare molto meglio su nidi perfetti che su nidi imperfetti.
Phil Miller

11

La stragrande maggioranza del codice che le persone scrivono sarà legato all'I / O (credo che tutto il codice che ho scritto per soldi negli ultimi 30 anni sia stato così vincolato), quindi le attività dell'ottimizzatore per la maggior parte delle persone saranno accademiche.

Tuttavia, vorrei ricordare alle persone che per ottimizzare il codice devi dire al compilatore di ottimizzarlo - molte persone (incluso me quando dimentico) postano benchmark C ++ qui che sono privi di significato senza l'ottimizzatore abilitato.


7
Confesso di essere peculiare: lavoro su grandi codici scientifici di elaborazione di numeri che sono vincolati alla larghezza di banda della memoria. Per la popolazione generale dei programmi sono d'accordo con Neil.
High Performance Mark

6
Vero; ma una gran parte di quel codice legato all'I / O oggigiorno è scritto in linguaggi che sono praticamente pessimizzatori , linguaggi che non hanno nemmeno compilatori. Sospetto che le aree in cui vengono ancora utilizzati C e C ++ tenderanno ad essere aree in cui è più importante ottimizzare qualcosa (utilizzo della CPU, utilizzo della memoria, dimensione del codice ...)
Porculus

3
Ho passato la maggior parte degli ultimi 30 anni a lavorare su codice con pochissimo I / O. Risparmia per 2 anni facendo database. Grafica, sistemi di controllo, simulazione: niente di tutto ciò legato all'I / O. Se l'I / O fosse il collo di bottiglia della maggior parte delle persone, non presteremmo molta attenzione a Intel e AMD.
phkahler

2
Sì, non mi piace molto questo argomento, altrimenti noi (nel mio lavoro) non cercheremmo modi per spendere più tempo di calcolo anche facendo I / O. Inoltre, gran parte del software legato all'I / O che ho incontrato è stato legato all'I / O perché l'I / O è stato eseguito in modo sciatto; se si ottimizzano i modelli di accesso (proprio come con la memoria), si possono ottenere enormi guadagni in termini di prestazioni.
dash-tom-bang

3
Di recente ho scoperto che quasi nessun codice scritto nel linguaggio C ++ è associato a I / O. Certo, se stai chiamando una funzione del sistema operativo per il trasferimento di massa del disco, il tuo thread potrebbe entrare in attesa di I / O (ma con la cache, anche questo è discutibile). Ma le solite funzioni di libreria di I / O, quelle che tutti consigliano perché sono standard e portatili, sono in realtà miseramente lente rispetto alla moderna tecnologia del disco (anche le cose a prezzo moderato). Molto probabilmente, l'I / O è il collo di bottiglia solo se stai scaricando tutto il percorso su disco dopo aver scritto solo pochi byte. OTOH, l'interfaccia utente è una questione diversa, noi umani siamo lenti.
Ben Voigt

11

utilizzare il più possibile la correttezza const nel codice. Consente al compilatore di ottimizzare molto meglio.

In questo documento ci sono molti altri suggerimenti per l'ottimizzazione: ottimizzazioni CPP (un documento un po 'vecchio però)

punti salienti:

  • utilizzare elenchi di inizializzazione del costruttore
  • utilizzare operatori di prefisso
  • utilizzare costruttori espliciti
  • funzioni inline
  • evitare oggetti temporanei
  • essere consapevoli del costo delle funzioni virtuali
  • restituire oggetti tramite parametri di riferimento
  • considerare l'assegnazione per classe
  • considera gli allocatori di contenitori stl
  • l'ottimizzazione del "membro vuoto"
  • eccetera

8
Non molto, raramente. Tuttavia, migliora la correttezza effettiva.
Potatoswatter

5
In C e C ++ il compilatore non può usare const per ottimizzare perché il casting è un comportamento ben definito.
dsimcha

+1: const è un buon esempio di qualcosa che avrà un impatto diretto sul codice compilato. Commento di re @ dsimcha - un buon compilatore verificherà se questo accade. Ovviamente, un buon compilatore "troverà" elementi const che non sono comunque dichiarati in quel modo ...
Hogan

@dsimcha: la modifica di un puntatore const e restrict qualificato, tuttavia, non è definita. Quindi un compilatore potrebbe ottimizzare in modo diverso in questo caso.
Dietrich Epp

6
Il casting di @dsimcha constsu un constriferimento o un constpuntatore a un non constoggetto è ben definito. modificare un constoggetto reale (cioè uno dichiarato come constoriginariamente) non lo è.
Stephen Lin

9

Tentare di programmare utilizzando il più possibile l'assegnazione singola statica. SSA è esattamente lo stesso di ciò che si ottiene nella maggior parte dei linguaggi di programmazione funzionali, ed è ciò in cui la maggior parte dei compilatori converte il codice per eseguire le ottimizzazioni perché è più facile lavorarci. In questo modo vengono portati alla luce i punti in cui il compilatore potrebbe confondersi. Inoltre, fa funzionare tutti, tranne i peggiori allocatori di registro, come i migliori allocatori di registro e ti consente di eseguire il debug più facilmente perché non devi quasi mai chiederti da dove una variabile ha ottenuto il suo valore poiché c'era solo un posto in cui era stata assegnata.
Evita le variabili globali.

Quando si lavora con i dati tramite riferimento o puntatore, inserirli nelle variabili locali, eseguire il proprio lavoro e quindi copiarli nuovamente. (a meno che tu non abbia una buona ragione per non farlo)

Utilizza il confronto quasi gratuito con 0 che la maggior parte dei processori ti offre quando esegui operazioni matematiche o logiche. Quasi sempre ottieni un flag per == 0 e <0, da cui puoi facilmente ottenere 3 condizioni:

x= f();
if(!x){
   a();
} else if (x<0){
   b();
} else {
   c();
}

è quasi sempre più economico del test per altre costanti.

Un altro trucco è usare la sottrazione per eliminare un confronto nel test di distanza.

#define FOO_MIN 8
#define FOO_MAX 199
int good_foo(int foo) {
    unsigned int bar = foo-FOO_MIN;
    int rc = ((FOO_MAX-FOO_MIN) < bar) ? 1 : 0;
    return rc;
} 

Questo molto spesso può evitare un salto in linguaggi che cortocircuitano le espressioni booleane ed evita che il compilatore debba cercare di capire come gestire il passo con il risultato del primo confronto mentre fa il secondo e poi combinarli. Potrebbe sembrare che abbia il potenziale per utilizzare un registro aggiuntivo, ma non lo fa quasi mai. Spesso non hai più bisogno di foo comunque, e se lo fai rc non è ancora usato quindi può andare lì.

Quando si utilizzano le funzioni stringa in c (strcpy, memcpy, ...) ricorda cosa restituiscono: la destinazione! Spesso è possibile ottenere un codice migliore "dimenticando" la propria copia del puntatore a destinazione e recuperandola semplicemente dal ritorno di queste funzioni.

Non trascurare mai l'opportunità di restituire esattamente la stessa cosa restituita dall'ultima funzione che hai chiamato. I compilatori non sono così bravi a capire che:

foo_t * make_foo(int a, int b, int c) {
        foo_t * x = malloc(sizeof(foo));
        if (!x) {
             // return NULL;
             return x; // x is NULL, already in the register used for returns, so duh
        }
        x->a= a;
        x->b = b;
        x->c = c;
        return x;
}

Ovviamente, potresti invertire la logica su questo se e avere solo un punto di ritorno.

(trucchi che ho ricordato più tardi)

Dichiarare le funzioni come statiche quando è possibile è sempre una buona idea. Se il compilatore può dimostrare a se stesso di aver tenuto conto di ogni chiamante di una particolare funzione, può infrangere le convenzioni di chiamata per quella funzione in nome dell'ottimizzazione. I compilatori possono spesso evitare di spostare i parametri nei registri o nelle posizioni dello stack in cui le funzioni chiamate di solito si aspettano che siano i loro parametri (deve deviare sia nella funzione chiamata che nella posizione di tutti i chiamanti per farlo). Il compilatore può anche trarre vantaggio dal sapere di quale memoria e registri la funzione chiamata avrà bisogno ed evitare di generare codice per preservare i valori delle variabili che si trovano nei registri o nelle posizioni di memoria che la funzione chiamata non disturba. Questo funziona particolarmente bene quando ci sono poche chiamate a una funzione.


2
In realtà non è necessario utilizzare la sottrazione durante il test degli intervalli, LLVM, GCC e il mio compilatore almeno lo fanno automaticamente. Poche persone probabilmente capirebbero cosa fa il codice con sottrazione e ancora meno perché funziona effettivamente.
Graziano Lup

nell'esempio sopra, b () non può essere chiamato perché se (x <0) verrà chiamato a ().
EvilTeach

@EvilTeach No, non lo farà. Il confronto che risulta nella chiamata a a () è! X
nategoose

@nategoose. se x è -3 allora! x è vero.
EvilTeach

@EvilTeach In C 0 è falso e tutto il resto è vero, quindi -3 è vero, quindi! -3 è falso
nategoose

9

Ho scritto un compilatore C ottimizzato e qui ci sono alcune cose molto utili da considerare:

  1. Rendi statica la maggior parte delle funzioni. Ciò consente alla propagazione delle costanti interprocedurali e all'analisi degli alias di svolgere il proprio lavoro, altrimenti il ​​compilatore deve presumere che la funzione possa essere chiamata dall'esterno dell'unità di traduzione con valori completamente sconosciuti per i parametri. Se guardi le famose librerie open-source, tutte contrassegnano le funzioni statiche tranne quelle che hanno davvero bisogno di essere esterne.

  2. Se vengono utilizzate variabili globali, contrassegnarle come statiche e costanti, se possibile. Se vengono inizializzati una volta (sola lettura), è meglio utilizzare un elenco di inizializzatori come static const int VAL [] = {1,2,3,4}, altrimenti il ​​compilatore potrebbe non scoprire che le variabili sono effettivamente costanti inizializzate e non riuscirà a sostituire i carichi dalla variabile con le costanti.

  3. NON usare MAI un goto all'interno di un ciclo, il ciclo non verrà più riconosciuto dalla maggior parte dei compilatori e nessuna delle ottimizzazioni più importanti verrà applicata.

  4. Utilizzare i parametri del puntatore solo se necessario e contrassegnarli come restrittivi se possibile. Questo aiuta molto l'analisi degli alias perché il programmatore garantisce che non ci siano alias (l'analisi degli alias interprocedurali è solitamente molto primitiva). Oggetti struct molto piccoli dovrebbero essere passati per valore, non per riferimento.

  5. Usa gli array invece dei puntatori quando possibile, specialmente all'interno dei loop (a [i]). Un array di solito offre più informazioni per l'analisi degli alias e dopo alcune ottimizzazioni verrà comunque generato lo stesso codice (cercare la riduzione della forza del loop se curioso). Ciò aumenta anche la possibilità di applicare il movimento del codice invariante al ciclo.

  6. Prova a sollevare chiamate al di fuori del ciclo per funzioni di grandi dimensioni o funzioni esterne che non hanno effetti collaterali (non dipendono dall'iterazione del ciclo corrente). Le funzioni piccole sono in molti casi incorporate o convertite in elementi intrinseci facili da sollevare, ma le funzioni di grandi dimensioni potrebbero sembrare che il compilatore abbia effetti collaterali quando in realtà non lo fanno. Gli effetti collaterali delle funzioni esterne sono completamente sconosciuti, ad eccezione di alcune funzioni della libreria standard che a volte sono modellate da alcuni compilatori, rendendo possibile il movimento del codice invariante al ciclo.

  7. Quando si scrivono test con più condizioni, posizionare prima quella più probabile. se (a || b || c) dovrebbe essere se (b || a || c) se b è più probabile che sia vero rispetto agli altri. I compilatori di solito non sanno nulla sui possibili valori delle condizioni e su quali rami vengono presi di più (potrebbero essere conosciuti utilizzando le informazioni del profilo, ma pochi programmatori lo usano).

  8. Usare uno switch è più veloce che fare un test come if (a || b || ... || z). Controlla prima se il tuo compilatore lo fa automaticamente, alcuni lo fanno ed è più leggibile avere il if però.


7

Nel caso di sistemi embedded e codice scritto in C / C ++, cerco di evitare il più possibile l' allocazione dinamica della memoria . Il motivo principale per cui lo faccio non è necessariamente la prestazione, ma questa regola pratica ha implicazioni sulle prestazioni.

Gli algoritmi utilizzati per gestire l'heap sono notoriamente lenti in alcune piattaforme (ad esempio, vxworks). Ancora peggio, il tempo necessario per tornare da una chiamata a malloc dipende in larga misura dallo stato corrente dell'heap. Pertanto, qualsiasi funzione che chiama malloc subirà un calo delle prestazioni che non può essere facilmente spiegato. Il calo delle prestazioni potrebbe essere minimo se l'heap è ancora pulito, ma dopo che il dispositivo viene eseguito per un po 'l'heap può diventare frammentato. Le chiamate impiegheranno più tempo e non è possibile calcolare facilmente come le prestazioni peggioreranno nel tempo. Non puoi davvero produrre una stima del caso peggiore. Anche in questo caso l'ottimizzatore non può fornire alcun aiuto. A peggiorare le cose, se l'heap diventa troppo frammentato, le chiamate inizieranno a fallire del tutto. La soluzione è usare i pool di memoria (ad es.fette glib ) invece dell'heap. Le chiamate di allocazione saranno molto più veloci e deterministiche se lo fai bene.


La mia regola pratica è che se devi allocare dinamicamente, ottieni un array in modo da non doverlo rifare. Preallocare i vettori.
EvilTeach

7

Un piccolo suggerimento stupido, ma che ti farà risparmiare quantità microscopiche di velocità e codice.

Passa sempre gli argomenti della funzione nello stesso ordine.

Se hai f_1 (x, y, z) che chiama f_2, dichiara f_2 come f_2 (x, y, z). Non dichiararlo come f_2 (x, z, y).

La ragione di ciò è che la piattaforma C / C ++ ABI (convenzione di chiamata AKA) promette di passare argomenti in particolari registri e posizioni di stack. Quando gli argomenti sono già nei registri corretti, non è necessario spostarli.

Durante la lettura del codice disassemblato ho visto alcuni ridicoli mescolarsi di registro perché le persone non seguivano questa regola.


2
Né C né C ++ forniscono alcuna garanzia, o addirittura menzionano, il passaggio di registri o posizioni di stack particolari. È l' ABI (es. Linux ELF) che determina i dettagli del passaggio dei parametri.
Emmet

5

Due tecniche di codifica che non ho visto nell'elenco sopra:

Bypass linker scrivendo codice come una fonte unica

Mentre la compilazione separata è davvero piacevole per il tempo di compilazione, è pessima quando parli di ottimizzazione. Fondamentalmente il compilatore non può ottimizzare oltre l'unità di compilazione, ovvero il dominio riservato del linker.

Ma se progetti bene il tuo programma puoi anche compilarlo attraverso un'unica fonte comune. Questo è invece di compilare unit1.c e unit2.c quindi collegare entrambi gli oggetti, compilare all.c che si limita a #include unit1.c e unit2.c. In questo modo trarrai vantaggio da tutte le ottimizzazioni del compilatore.

È molto come scrivere intestazioni solo per programmi in C ++ (e ancora più facile da fare in C).

Questa tecnica è abbastanza semplice se scrivi il tuo programma per abilitarlo dall'inizio, ma devi anche essere consapevole che cambia parte della semantica C e puoi incontrare alcuni problemi come variabili statiche o macro collisioni. Per la maggior parte dei programmi è abbastanza facile superare i piccoli problemi che si verificano. Inoltre, tieni presente che la compilazione come fonte unica è molto più lenta e può richiedere un'enorme quantità di memoria (di solito non è un problema con i sistemi moderni).

Usando questa semplice tecnica mi è capitato di realizzare alcuni programmi che ho scritto dieci volte più velocemente!

Come la parola chiave register, anche questo trucco potrebbe diventare presto obsoleto. L'ottimizzazione tramite linker inizia ad essere supportata dai compilatori gcc: Ottimizzazione del tempo di collegamento .

Separare le attività atomiche in cicli

Questo è più complicato. Riguarda l'interazione tra la progettazione dell'algoritmo e il modo in cui l'ottimizzatore gestisce la cache e l'allocazione dei registri. Molto spesso i programmi devono eseguire il loop su una struttura di dati e per ogni elemento eseguire alcune azioni. Molto spesso le azioni eseguite possono essere suddivise tra due attività logicamente indipendenti. In tal caso, è possibile scrivere esattamente lo stesso programma con due cicli sullo stesso confine eseguendo esattamente un compito. In alcuni casi scriverlo in questo modo può essere più veloce del ciclo unico (i dettagli sono più complessi, ma una spiegazione può essere che con il task case semplice tutte le variabili possono essere mantenute nei registri del processore e con quello più complesso non è possibile e alcuni i registri devono essere scritti in memoria e riletti in un secondo momento e il costo è superiore al controllo di flusso aggiuntivo).

Fai attenzione a questo (prestazioni del profilo che usano questo trucco o meno) poiché come usare il registro potrebbe anche dare prestazioni inferiori rispetto a quelle migliorate.


2
Sì, ormai, LTO ha reso la prima metà di questo post ridondante e probabilmente un cattivo consiglio.
underscore_d

@underscore_d: ci sono ancora alcuni problemi (principalmente legati alla visibilità dei simboli esportati), ma da un mero punto di vista delle prestazioni probabilmente non c'è più bisogno.
kriss

4

In realtà l'ho visto fare in SQLite e affermano che si traduce in aumenti delle prestazioni del ~ 5%: Metti tutto il tuo codice in un file o usa il preprocessore per fare l'equivalente. In questo modo l'ottimizzatore avrà accesso all'intero programma e potrà eseguire più ottimizzazioni interprocedurali.


5
Mettere le funzioni che vengono utilizzate insieme in stretta vicinanza fisica nel sorgente aumenta la probabilità che siano vicine l'una all'altra nei file oggetto e vicine l'una all'altra nel tuo eseguibile. Questa migliore località delle istruzioni può aiutare a evitare errori nella cache delle istruzioni durante l'esecuzione.
paxos1977

Il compilatore AIX dispone di un'opzione del compilatore per incoraggiare tale comportamento -qipa [= <suboptions_list>] | -qnoipa Attiva o personalizza una classe di ottimizzazioni note come analisi interprocedurale (IPA).
EvilTeach

4
La cosa migliore è avere un modo per svilupparsi che non lo richieda. Usare questo fatto come scusa per scrivere codice non modulare risulterà nel complesso solo in codice lento e con problemi di manutenzione.
Hogan

3
Penso che questa informazione sia leggermente datata. In teoria, le funzionalità di ottimizzazione dell'intero programma integrate in molti compilatori ora (ad es. "Ottimizzazione del tempo di collegamento" in gcc) consentono gli stessi vantaggi, ma con un flusso di lavoro totalmente standard (più tempi di ricompilazione più rapidi rispetto a mettere tutto in un file !)
Ponkadoodle

@Wallacoloo Di sicuro, questa è una data molto lontana. FWIW, ho usato l'LTO di GCC per la prima volta oggi e, a parità di tutto il resto -O3, ha eliminato il 22% delle dimensioni originali dal mio programma. (Non è limitato alla CPU, quindi non ho molto da dire sulla velocità.)
underscore_d

4

La maggior parte dei compilatori moderni dovrebbe fare un buon lavoro accelerando la ricorsione in coda , perché le chiamate di funzione possono essere ottimizzate.

Esempio:

int fac2(int x, int cur) {
  if (x == 1) return cur;
  return fac2(x - 1, cur * x); 
}
int fac(int x) {
  return fac2(x, 1);
}

Ovviamente questo esempio non ha alcun controllo dei limiti.

Modifica tardiva

Anche se non ho una conoscenza diretta del codice; sembra chiaro che i requisiti per l'utilizzo di CTE su SQL Server sono stati specificamente progettati in modo che possa essere ottimizzato tramite la ricorsione tail-end.


1
la domanda riguarda C. C non rimuove la ricorsione della coda, quindi coda o altra ricorsione, lo stack potrebbe saltare se la ricorsione è troppo profonda.
Toad

1
Ho evitato il problema della convenzione di chiamata utilizzando goto. In questo modo ci sono meno spese generali.
EvilTeach

2
@hogan: questo è nuovo per me. Potresti indicare un compilatore che fa questo? E come puoi essere sicuro che lo ottimizzi effettivamente? Se lo fa, è davvero necessario essere sicuri che lo faccia. Non è qualcosa che speri che l'ottimizzatore del compilatore raccolga (come l'inlining che può o non può funzionare)
Toad

6
@hogan: sono corretto. Hai ragione che Gcc e MSVC fanno entrambi l'ottimizzazione della ricorsione della coda.
Toad

5
Questo esempio non è la ricorsione della coda in quanto non è la chiamata ricorsiva che è l'ultima, è la moltiplicazione.
Brian Young

4

Non fare lo stesso lavoro più e più volte!

Un antipattern comune che vedo segue queste linee:

void Function()
{
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomething();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingElse();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingCool();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingReallyNeat();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingYetAgain();
}

Il compilatore deve effettivamente chiamare tutte quelle funzioni tutto il tempo. Supponendo che tu, il programmatore, sappia che l'oggetto aggregato non cambia nel corso di queste chiamate, per amore di tutto ciò che è sacro ...

void Function()
{
   MySingleton* s = MySingleton::GetInstance();
   AggregatedObject* ao = s->GetAggregatedObject();
   ao->DoSomething();
   ao->DoSomethingElse();
   ao->DoSomethingCool();
   ao->DoSomethingReallyNeat();
   ao->DoSomethingYetAgain();
}

Nel caso del singleton getter le chiamate potrebbero non essere troppo costose, ma è certamente un costo (tipicamente, "controlla per vedere se l'oggetto è stato creato, se non lo è, crealo e poi restituiscilo). più complicata diventa questa catena di getter, più tempo perderemo.


3
  1. Utilizzare l'ambito più locale possibile per tutte le dichiarazioni di variabili.

  2. Usa constquando possibile

  3. Non utilizzare il registro a meno che non si preveda di profilare sia con che senza

I primi 2 di questi, in particolare il numero 1, aiutano l'ottimizzatore ad analizzare il codice. In particolare, lo aiuterà a fare buone scelte su quali variabili tenere nei registri.

L'uso ciecamente della parola chiave register può aiutare tanto quanto danneggiare la tua ottimizzazione, è semplicemente troppo difficile sapere cosa sarà importante finché non guardi l'output o il profilo dell'assembly.

Ci sono altre cose che contano per ottenere buone prestazioni dal codice; progettare le strutture dei dati per massimizzare la coerenza della cache, ad esempio. Ma la domanda riguardava l'ottimizzatore.



3

Mi è venuto in mente qualcosa che ho incontrato una volta, in cui il sintomo era semplicemente che stavamo esaurendo la memoria, ma il risultato è stato un aumento sostanziale delle prestazioni (oltre a enormi riduzioni dell'impronta di memoria).

Il problema in questo caso era che il software che stavamo usando faceva tantissime piccole allocazioni. Ad esempio, allocare quattro byte qui, sei byte là, ecc. Anche molti piccoli oggetti, in esecuzione nell'intervallo di 8-12 byte. Il problema non era tanto che il programma avesse bisogno di molte piccole cose, ma che assegnava molte piccole cose individualmente, il che ha gonfiato ogni allocazione (su questa particolare piattaforma) 32 byte.

Parte della soluzione era mettere insieme un pool di piccoli oggetti in stile Alexandrescu, ma estenderlo in modo da poter allocare array di piccoli oggetti oltre a singoli elementi. Ciò ha aiutato immensamente anche le prestazioni poiché più elementi si adattano alla cache in qualsiasi momento.

L'altra parte della soluzione era sostituire l'uso dilagante di membri char * gestiti manualmente con una stringa SSO (small-string optimization). Essendo l'allocazione minima di 32 byte, ho creato una classe di stringhe che aveva un buffer di 28 caratteri incorporato dietro un carattere *, quindi il 95% delle nostre stringhe non ha avuto bisogno di un'allocazione aggiuntiva (e quindi ho sostituito manualmente quasi ogni aspetto di char * in questa libreria con questa nuova classe, divertente o meno). Ciò ha aiutato moltissimo anche con la frammentazione della memoria, che ha poi aumentato la località di riferimento per altri oggetti puntati, e allo stesso modo ci sono stati miglioramenti delle prestazioni.


3

Una tecnica accurata che ho appreso dal commento di @MSalters su questa risposta consente ai compilatori di eseguire l'elisione della copia anche quando restituiscono oggetti diversi in base a qualche condizione:

// before
BigObject a, b;
if(condition)
  return a;
else
  return b;

// after
BigObject a, b;
if(condition)
  swap(a,b);
return a;

2

Se hai piccole funzioni che chiami ripetutamente, in passato ho ottenuto grandi guadagni inserendole nelle intestazioni come "inline statico". Le chiamate alle funzioni sull'ix86 sono sorprendentemente costose.

Anche la reimplementazione di funzioni ricorsive in modo non ricorsivo usando uno stack esplicito può guadagnare molto, ma in questo caso sei davvero nel regno del tempo di sviluppo rispetto al guadagno.


La conversione della ricorsione in uno stack è un'ottimizzazione presunta su ompf.org, per le persone che sviluppano raytracer e scrivono altri algoritmi di rendering.
Tom

... dovrei aggiungere a questo, che il più grande overhead nel mio progetto personale di raytracer è la ricorsione basata su vtable attraverso una gerarchia di volume di delimitazione usando il pattern Composite. In realtà è solo un mucchio di scatole nidificate strutturate come un albero, ma l'uso del modello causa il gonfiore dei dati (puntatori a tabelle virtuali) e riduce la coerenza delle istruzioni (quello che potrebbe essere un ciclo piccolo / stretto è ora una catena di chiamate di funzione)
Tom

2

Ecco il mio secondo consiglio per l'ottimizzazione. Come per il mio primo consiglio, questo è per scopi generali, non specifico per linguaggio o processore.

Leggi attentamente il manuale del compilatore e capisci cosa ti dice. Usa il compilatore al massimo.

Sono d'accordo con uno o due degli altri intervistati che hanno identificato la selezione del giusto algoritmo come fondamentale per spremere le prestazioni da un programma. Oltre a ciò, il tasso di rendimento (misurato nel miglioramento dell'esecuzione del codice) sul tempo investito nell'utilizzo del compilatore è di gran lunga superiore al tasso di rendimento nel modificare il codice.

Sì, gli autori di compilatori non provengono da una razza di giganti del codice e i compilatori contengono errori e ciò che dovrebbe, secondo il manuale e secondo la teoria del compilatore, rendere le cose più veloci a volte le rende più lente. Ecco perché devi fare un passo alla volta e misurare le prestazioni prima e dopo il tweak.

E sì, in definitiva, potresti trovarti di fronte a un'esplosione combinatoria di flag del compilatore, quindi devi avere uno o due script per eseguire make con vari flag del compilatore, mettere in coda i lavori sul cluster di grandi dimensioni e raccogliere le statistiche di runtime. Se sei solo tu e Visual Studio su un PC, l'interesse sarà esaurito molto prima di aver provato combinazioni sufficienti di flag di compilazione sufficienti.

Saluti

marchio

Quando prendo per la prima volta un pezzo di codice, di solito posso ottenere un fattore di 1,4 - 2,0 volte più prestazioni (cioè la nuova versione del codice viene eseguita in 1 / 1,4 o 1/2 del tempo della vecchia versione) entro un giorno o due armeggiando con i flag del compilatore. Certo, questo potrebbe essere un commento sulla mancanza di conoscenza del compilatore tra gli scienziati che hanno creato gran parte del codice su cui lavoro, piuttosto che un sintomo della mia eccellenza. Avendo impostato i flag del compilatore al massimo (e raramente è solo -O3) possono essere necessari mesi di duro lavoro per ottenere un altro fattore di 1.05 o 1.1


2

Quando DEC è uscito con i suoi processori alpha, c'era una raccomandazione per mantenere il numero di argomenti per una funzione sotto 7, poiché il compilatore cercava sempre di inserire automaticamente fino a 6 argomenti nei registri.


x86-64 bit consente anche molti parametri passati al registro, che possono avere un effetto notevole sull'overhead delle chiamate di funzione.
Tom

1

Per le prestazioni, concentrati prima sulla scrittura di codice manutenibile - componentizzato, liberamente accoppiato, ecc., Quindi quando devi isolare una parte per riscrivere, ottimizzare o semplicemente profilare, puoi farlo senza troppi sforzi.

L'ottimizzatore aiuterà marginalmente le prestazioni del tuo programma.


3
Questo funziona solo se le stesse "interfacce" di accoppiamento possono essere ottimizzate. Un'interfaccia può essere intrinsecamente "lenta", ad esempio forzando ricerche o calcoli ridondanti o forzando un accesso errato alla cache.
Tom il

1

Stai ottenendo buone risposte qui, ma presumono che il tuo programma sia abbastanza vicino all'ottimale per cominciare, e dici

Supponiamo che il programma sia stato scritto correttamente, compilato con piena ottimizzazione, testato e messo in produzione.

Nella mia esperienza, un programma può essere scritto correttamente, ma ciò non significa che sia quasi ottimale. Ci vuole del lavoro extra per arrivare a quel punto.

Se posso fare un esempio, questa risposta mostra come un programma dall'aspetto perfettamente ragionevole è stato reso oltre 40 volte più veloce dall'ottimizzazione macro . Non è possibile eseguire grandi accelerazioni in tutti i programmi come è stato scritto per la prima volta, ma in molti (ad eccezione di programmi molto piccoli) è possibile, secondo la mia esperienza.

Dopodiché, la microottimizzazione (degli hot-spot) può darti un buon guadagno.


1

io uso il compilatore Intel. sia su Windows che su Linux.

quando più o meno fatto profilo il codice. quindi aggrappati agli hotspot e prova a cambiare il codice per consentire al compilatore di fare un lavoro migliore.

se un codice è di tipo computazionale e contiene molti cicli - il rapporto di vettorizzazione nel compilatore Intel è molto utile - cerca "vec-report" nella guida.

quindi l'idea principale: lucidare il codice critico per le prestazioni. per il resto - priorità per essere corretti e manutenibili - funzioni brevi, codice chiaro che potrebbe essere compreso 1 anno dopo.


Ti stai avvicinando a rispondere alla domanda ..... che genere di cose fai al codice, per consentire al compilatore di fare questo tipo di ottimizzazioni?
EvilTeach

1
Cercando di scrivere di più in C-style (rispetto a C ++) ad es. Evitando le funzioni virtuali senza bisogno assoluto, specialmente se verranno chiamate spesso, evita AddRefs .. e tutte le cose interessanti (di nuovo a meno che non sia davvero necessario). Scrivi codice facile per l'inlining: meno parametri, meno "if" -s. Non utilizzare variabili globali a meno che non sia assolutamente necessario. Nella struttura dati - metti prima i campi più ampi (double, int64 va prima di int) - quindi il compilatore allinea la struttura alla dimensione naturale del primo campo - allineamento buono per perf.
jf.

1
Il layout e l'accesso ai dati sono assolutamente fondamentali per le prestazioni. Quindi, dopo la profilazione, a volte divido una struttura in più strutture in base alla località degli accessi. Un altro trucco generale: usa int o size-t rispetto a char - anche i valori dei dati sono piccoli - evita vari perf. sanzioni memorizzazione per blocco del carico, problemi con stallo dei registri parziali. ovviamente questo non è applicabile quando sono necessari grandi array di tali dati.
jf.

Ancora una: evita le chiamate di sistema, a meno che non ce ne sia una reale necessità :) - sono MOLTO costose
jf.

2
@jf: ho fatto +1 sulla tua risposta, ma per favore potresti spostare la risposta dai commenti al corpo della risposta? Sarà più facile da leggere.
kriss

1

Un'ottimizzazione che ho usato in C ++ è la creazione di un costruttore che non fa nulla. Si deve chiamare manualmente un init () per mettere l'oggetto in uno stato funzionante.

Questo ha un vantaggio nel caso in cui ho bisogno di un grande vettore di queste classi.

Chiamo reserve () per allocare lo spazio per il vettore, ma il costruttore non tocca effettivamente la pagina di memoria su cui si trova l'oggetto. Quindi ho speso un po 'di spazio per gli indirizzi, ma in realtà non ho consumato molta memoria fisica. Evito i difetti di pagina associati ai costi di costruzione associati.

Mentre genero oggetti per riempire il vettore, li imposto usando init (). Questo limita i miei errori di pagina totali ed evita la necessità di ridimensionare () il vettore durante il riempimento.


6
Credo che un'implementazione tipica di std :: vector in realtà non costruisca più oggetti quando riservi () più capacità. Alloca solo le pagine. I costruttori vengono chiamati in seguito, usando il posizionamento new, quando si aggiungono effettivamente oggetti al vettore - che è (presumibilmente) subito prima di chiamare init (), quindi non è realmente necessaria la funzione separata init (). Ricorda anche che anche se il tuo costruttore è "vuoto" nel codice sorgente, il costruttore compilato può contenere codice per inizializzare cose come tabelle virtuali e RTTI, quindi le pagine vengono comunque toccate al momento della costruzione.
Wyzard

1
Sì. Nel nostro caso usiamo push_back per popolare il vettore. Gli oggetti non hanno funzioni virtuali, quindi non è un problema. La prima volta che l'abbiamo provato con il costruttore, siamo rimasti sbalorditi dal volume di errori di pagina. Mi sono reso conto di quello che è successo, abbiamo strappato le viscere del costruttore e il problema degli errori di pagina è svanito.
EvilTeach

Questo piuttosto mi sorprende. Quali implementazioni C ++ e STL stavi utilizzando?
David Thornley

3
Sono d'accordo con gli altri, suona come una cattiva implementazione di std :: vector. Anche se i tuoi oggetti avessero vtable, non verrebbero costruiti fino al tuo push_back. Dovresti essere in grado di testarlo dichiarando privato il costruttore predefinito, perché tutto il vettore di cui avrà bisogno è il costruttore di copia per push_back.
Tom

1
@David - L'implementazione era su AIX.
EvilTeach

1

Una cosa che ho fatto è cercare di mantenere le azioni costose in luoghi in cui l'utente potrebbe aspettarsi che il programma ritardi un po '. Le prestazioni complessive sono legate alla reattività, ma non sono esattamente le stesse, e per molte cose la reattività è la parte più importante delle prestazioni.

L'ultima volta che ho dovuto apportare miglioramenti alle prestazioni complessive, ho tenuto d'occhio gli algoritmi non ottimali e ho cercato luoghi che avrebbero avuto problemi di cache. Ho analizzato e misurato le prestazioni prima e di nuovo dopo ogni modifica. Poi l'azienda è crollata, ma è stato comunque un lavoro interessante e istruttivo.


0

Ho a lungo sospettato, ma non ho mai dimostrato che la dichiarazione di array in modo che contengano una potenza di 2, come numero di elementi, consente all'ottimizzatore di ridurre la forza sostituendo una moltiplicazione con uno spostamento di un numero di bit, quando si cerca singoli elementi.


6
Questo era vero, oggi lo è più. Infatti è vero esattamente il contrario. Se dichiari i tuoi array con potenze di due, molto probabilmente ti imbatterai nella situazione in cui lavori su due puntatori a potenza di due separati in memoria. Il problema è che le cache della CPU sono organizzate in questo modo e potresti finire con i due array che combattono attorno a una riga della cache. In questo modo ottieni prestazioni orribili. Avere uno dei puntatori un paio di byte avanti (ad esempio, non potenza di due) impedisce questa situazione.
Nils Pipenbrinck

+1 Nils, e un'occorrenza specifica di questo è "64k aliasing" su hardware Intel.
Tom

Questo è qualcosa che è facilmente smentito guardando lo smontaggio, tra l'altro. Sono rimasto sbalordito, anni fa, nel vedere come gcc avrebbe ottimizzato tutti i tipi di moltiplicazioni costanti con turni e aggiunte. Ad esempio val * 7trasformato in quello che altrimenti sarebbe (val << 3) - val.
dash-tom-bang

0

Metti le funzioni piccole e / o chiamate di frequente all'inizio del file sorgente. Ciò rende più facile per il compilatore trovare opportunità per l'inlining.


Veramente? Puoi citare una motivazione ed esempi per questo? Non sto dicendo che non è vero, ma sembra poco intuitivo che la posizione sarebbe importante.
underscore_d

@underscore_d non può incorporare qualcosa finché non si conosce la definizione della funzione. Sebbene i compilatori moderni possano eseguire più passaggi in modo che la definizione sia nota al momento della generazione del codice, non lo presumo.
Mark Ransom

Pensavo che i compilatori funzionassero su grafici di chiamate astratti piuttosto che sull'ordine delle funzioni fisiche, il che significa che non avrebbe importanza. Certo, suppongo che non faccia male stare molto attenti, specialmente quando, prestazioni a parte, IMO sembra semplicemente più logico definire le funzioni che vengono chiamate prima di quelle che le chiamano. Dovrei testare le prestazioni ma sarei sorpreso se fosse importante, ma fino ad allora sono aperto a essere sorpreso!
underscore_d
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.