2009-02-27 12 views

risposta

19
gcc -O2 

I compilatori fanno molto più lavoro di quanto si possa.

+3

"... di quanto puoi." <- caso generale. In alcuni casi specifici (come algoritmi, DSP, ecc.) Un essere umano può codificare una routine C che sembra essere piuttosto strana, ma una volta compilata genera un assembly migliore per lo scopo specifico rispetto al compilatore. –

+0

Principalmente dovuto, tuttavia, al fatto che anche le ottimizzazioni del compilatore ottimizzano solo determinati tipi di ottimizzazione e sezioni più piccole di codice ottimizzato. Una volta compreso il compilatore e l'assembly, è possibile ottimizzare a mano pezzi di codice molto più grandi che il compilatore non potrebbe rendere migliori. –

+0

... ma sto dividendo i capelli: poche persone avrebbero mai avuto bisogno di farlo. È divertente vedere come un compilatore ha trasformato una sezione di codice in assembly - alcune delle ottimizzazioni del compilatore sono in realtà abbastanza intricate e dispari finché non lo si studia davvero. –

6

++i può essere più veloce di i++, perché evita la creazione di una temporanea.

Se questo è ancora valido per i compilatori C/C++/Java/C# moderni, non lo so. Potrebbe essere diverso per i tipi definiti dall'utente con operatori sovraccaricati, mentre nel caso di numeri interi semplici probabilmente non ha importanza.

Ma a me è piaciuta la sintassi ... si legge come "incrementa i" che è un ordine ragionevole.

+1

i compilatori più moderni non creeranno il temporaneo se è usato solo come un'istruzione, e non come un'espressione. – Javier

15

Sollevare una potenza di due per filtri, tamponi circolari ecc

Quindi molto, molto comodo.

-Adam

+0

Qualcuno in poche parole può spiegare qual è il trucco dietro? Lo incontri sempre, ma non l'ho mai capito ... – daspostloch

+2

@daspostloch - L'idea è che spesso devi eseguire il controllo dei confini e il troncamento su cose che accedono ai dati. Questo significa fare 'if (input> MAX_SIZE) input = input - MAX_SIZE;' per esempio. Tuttavia, se la struttura ha una potenza di due dimensioni, allora sia il controllo che il calcolo matematico possono essere eseguiti con un'operazione 'AND'. Ad esempio, se la dimensione dei dati è 128, allora 'input = input & 0x7F;' troncherà tutto sopra 128, e altrimenti lascerà 'input' da solo, il che significa che l'istruzione' if' può essere rimossa in precedenza. –

+0

grazie, un foro cucito :) – daspostloch

2

Conto alla rovescia un ciclo. E 'più economico da confrontare con 0 di N:

for (i = N; --i >= 0;) ... 

spostamento e mascheramento per potenze di due è più conveniente di divisione e resto,/e%

#define WORD_LOG 5 
#define SIZE (1 << WORD_LOG) 
#define MASK (SIZE - 1) 

uint32_t bits[K] 

void set_bit(unsigned i) 
{ 
    bits[i >> WORD_LOG] |= (1 << (i & MASK)) 
} 

Modifica

(i >> WORD_LOG) == (i/SIZE) and 
(i & MASK) == (i % SIZE) 

perché SIZE è 32 o 2^5.

+0

I compilatori sono in grado di convertire automaticamente un ciclo nel modulo di conto alla rovescia più veloce, se la variabile indice non viene utilizzata in nessuna espressione. –

+0

mi piace il conteggio alla rovescia; ma soprattutto perché rende while() {..} loops più bello di (;;) {...} – Javier

+0

I compilatori sono abbastanza intelligenti al giorno d'oggi nell'implementare le divisioni con una costante usando turni e altri trucchi (vedi http: // hexblog. com/2005/11/do_you_know_the_division_opera.html). Ma poi, a volte non lo sono (http://hexblog.com/2005/12/the_longest_arithmetic_operati.html) –

2
  • Riciclaggio del frame-pointer all'improvviso
  • Pascal chiamante convenzione
  • riscrittura stack frame di chiamata coda optimizarion (anche se talvolta pasticci con quanto sopra)
  • Utilizzando vfork() anziché fork() prima exec()
  • E quello che sto ancora cercando, una scusa per usare: i dati guidata di generazione di codice a runtime
+0

le moderne implementazioni di fork() utilizzano la copia su scrittura, che insieme a alcuni hack largamente usati, lo rendono veloce quanto vfork(). in Linux, vfork() chiama clone(), proprio come fork() – Javier

+0

vfork() sarà sempre almeno un pagefault più veloce, e vedrai il flag su clone() che è CLONE_VFORK. Lo spazio di memoria del processo in-parent è ancora lì. Provalo con una variabile volatile se non mi credi. – Joshua

+0

+1 per la generazione del codice, se alcuni dati cambiano molto di rado, è una grande vittoria. Non solo più veloce, ma forse sorprendentemente, più semplice da scrivere. –

9

Ispeziona l'output del compilatore, quindi prova a forzarlo a fare qualcosa più velocemente.

+0

sì, non c'è niente di meglio per eliminare un po 'di tempo libero. basta fare attenzione a rendere il codice sorgente più leggibile, non meno. (ne aggiunge alcuni alla sfida :-) – Javier

+0

Con i processori odierni, non è possibile sapere cosa è più veloce guardando l'output del compilatore. Se lo profili in modi diversi, potresti essere in grado di dire perché un metodo è più veloce di un altro, ma potrebbe non essere applicabile al prossimo pezzo di codice. –

+0

È possibile se il compilatore emette pattern opcode noti per essere lenti. Ad esempio, spostamenti variabili su un chip PPC o su un numero qualsiasi di load-hit-store. È meno utile nel caso generale, ma per gli hotspot è decisamente utile. – MSN

3

Allocazione con nuovo su un buffer preassegnato utilizzando il posizionamento di C++ nuovo.

7

Utilizzo della metaprogrammazione del modello per calcolare le cose in fase di compilazione anziché in fase di esecuzione.

+0

questo è quello che mi piace dei linguaggi di scripting, puoi fare molti calcoli al momento del caricamento per rendere più veloce il runtime successivo.concesso, il "tempo di caricamento" è in realtà solo una parte del runtime, ma continuiamo a separare i problemi di prestazioni. – Javier

+0

Mai pensato a questo. Hai qualche esempio? –

12

Uno dei più utili nel codice scientifico è quello di sostituire pow(x,4) con x*x*x*x.Pow è quasi sempre più costoso della moltiplicazione. Questo è seguito da

for(int i = 0; i < N; i++) 
    { 
    z += x/y; 
    } 

a

double denom = 1/y; 
    for(int i = 0; i < N; i++) 
    { 
    z += x*denom; 
    } 

Ma il mio preferito di ottimizzazione a basso livello è quello di capire quali calcoli possono essere rimossi da un ciclo. È sempre più veloce eseguire il calcolo una volta anziché N volte. A seconda del compilatore, alcuni di questi potrebbero essere fatti automaticamente per te.

+0

tutti i compilatori leggermente ottimizzanti eseguono almeno l'eliminazione del codice morto e il movimento del codice loop-invariante (quello che descrivete qui). ancora, anch'io tendo a farlo manualmente. soprattutto se rende l'algoritmo più chiaro. – Javier

+0

Che ci crediate o no, ho visto guadagni di prestazioni effettive quando il denominatore è leggermente più complicato. Anche con il compilatore Intel. Inoltre, non sto parlando di codice morto, ma di codice che non ha bisogno di essere eseguito all'interno del ciclo. – Steve

+0

Penso che i compilatori moderni possano facilmente fare questo tipo di ottimizzazioni – user

2

In SQL, se avete solo bisogno di sapere se esiste o meno tutti i dati, non perdere tempo con COUNT(*):

SELECT 1 FROM table WHERE some_primary_key = some_value 

Se la clausola WHERE è probabile tornare più righe, aggiungere un LIMIT 1 troppo.

(Ricordate che i database non possono vedere che cosa il vostro codice sta facendo con i loro risultati, in modo che non possono ottimizzare queste cose via da soli!)

+0

Perché non lanciare il LIMITE 1 a prescindere? – strager

+0

Sì, sempre LIMIT 1/TOP 1 qui. Fast-first-row è quello che vuoi. – Joshua

+0

Ho dato un'occhiata al piano di query di Postgres con e senza, e mettere un limite su una selezione già a riga singola sembra aggiungere un sovraccarico. Forse è diverso per gli altri sistemi DB. – flussence

2

Ho scoperto che il passaggio da un puntatore ad accesso indicizzato può fare la differenza; il compilatore ha diverse forme di istruzione e registra gli usi tra cui scegliere. Viceversa, anche. Questo è estremamente basso livello e dipendente dal compilatore, però, e va bene solo quando serve l'ultimo percento.

E.g.

for (i = 0; i < n; ++i) 
    *p++ = ...; // some complicated expression 

vs.

for (i = 0; i < n; ++i) 
    p[i] = ...; // some complicated expression 
+0

Quello mi sembra abbastanza ovvio, due incrementi rispetto a uno. Che ne dici di mettere 'p ++' all'interno di 'for()' e di abbandonare 'i' del tutto? – flussence

+0

Intendi per (fine = p + n; p! = Fine; ++ p)? Potrebbe funzionare. Non potrebbe però, perché il compilatore potrebbe ottimizzare il ciclo 'i'. Devi davvero provare queste cose e vedere quale è il più veloce, perché ci sono troppe variabili. –

5

anni fa con un compilier non-così-intelligente, ho ricevuto grande distanza in miglia dalla funzione inline, camminando puntatori invece di array di indicizzazione, e l'iterazione fino a zero, invece di up al massimo.

In caso di dubbio, un po 'di conoscenza di assemblea vi permetterà di guardare a ciò che il compilatore sta producendo e attaccare le parti inefficienti (nella lingua di partenza, utilizzando le strutture più amichevole per il vostro compilatore.)

+0

Con gli odierni processori, guardare l'assemblea ti porterà solo lontano. Hai davvero bisogno di cronometrare. –

+0

Buon punto. Stavo pensando a un caso in cui il compilatore utilizzava la RAM quando c'erano molti registri. Ho finito per riscrivere quel programma in assembly, ma i sistemi embedded sono una specie di un mondo diverso. –

+0

Sono d'accordo. E non mi piace un compilatore troppo intelligente. Voglio solo che sia un buon ASM per me. –

8

non lo farei necessariamente lo definisco un'ottimizzazione di basso livello, ma ho risparmiato ordini di grandezza di più cicli attraverso un'applicazione giudiziosa del caching di quante ne abbia in tutte le mie applicazioni di trucchi di basso livello combinati. Molti di questi metodi sono specifici per le applicazioni.

  • Avere una cache LRU di query di database (o qualsiasi altra richiesta basata su IPC).
  • Memorizzare l'ultima query del database non riuscita e restituire un errore se richiesto nuovamente entro un determinato intervallo di tempo.
  • Ricordare la posizione in una struttura di dati di grandi dimensioni per garantire che se la richiesta successiva è per lo stesso nodo, la ricerca è gratuita.
  • Risultati del calcolo di memorizzazione nella cache per impedire il doppio lavoro. Oltre agli scenari più complessi, questo si trova spesso nelle dichiarazioni if o for.

CPU e compilatori cambiano continuamente. Qualunque trucco di codice di basso livello che avesse senso 3 chip della CPU fa con un compilatore diverso potrebbe in effetti essere più lento nell'architettura corrente e ci potrebbero essere buone probabilità che questo trucco possa confondere chiunque stia mantenendo questo codice in futuro.

+0

In qualsiasi cache non banale, è necessario preoccuparsi della gestione della cache: dimensioni, staleness, invalidation, correctness. Che aggiunge sovraccarico, complessità e una nuova fonte di bug. Ancora, il caching giudizioso è enormemente efficace. –

5

valori di calcolo preliminare.

Ad esempio, invece di sin (a) o cos (a), se l'applicazione non ha necessariamente bisogno di angoli per essere molto precisi, forse si rappresentano gli angoli in 1/256 di un cerchio e si creano matrici di galleggianti seno [] e coseno [] precalcolano il peccato e il cos di quegli angoli.

E, se è necessario frequentemente un vettore con un certo angolo di una determinata lunghezza, è possibile calcolare preventivamente tutti quei seni e coseni già moltiplicati per quella lunghezza.

Oppure, per dirla in modo più generale, scambia la memoria per la velocità.

O, anche, più in generale, "Tutta la programmazione è un esercizio di caching" - Terje Mathisen

Alcune cose sono meno evidenti. Per esempio attraversare una matrice bidimensionale, si potrebbe fare qualcosa di simile

 
    for (x=0;x<maxx;x++) 
     for (y=0;y<maxy;y++) 
      do_something(a[x,y]); 

Si potrebbe trovare la cache del processore piace meglio se lo fai:

 
    for (y=0;y<maxy;y++) 
     for (x=0;x<maxx;x++) 
      do_something(a[x,y]); 

o viceversa.

5

Non eseguire cicli di srotolamento. Non fare il dispositivo di Duff. Rendi il tuo loop il più piccolo possibile, qualsiasi altra cosa inibisce le prestazioni x86 e le prestazioni dell'ottimizzatore gcc.

L'eliminazione dei rami può essere utile, tuttavia, eliminare completamente i loop è una buona cosa, e quei trucchi matematici senza rami funzionano davvero. Oltre a ciò, cerca di non uscire mai dalla cache L2 - questo significa che si dovrebbe evitare un sacco di precalcolo/memorizzazione nella cache se si spreca spazio nella cache.

E, soprattutto per x86, provare a mantenere il numero di variabili in uso in qualsiasi momento. È difficile dire cosa faranno i compilatori con quel genere di cose, ma di solito con meno variabili di iterazione di loop/indici di array si otterrà un output di asm migliore.

Naturalmente, questo è per le CPU desktop; una CPU lenta con accesso veloce alla memoria può precalcolare molto di più, ma in questi giorni potrebbe essere un sistema integrato con poca memoria totale comunque ...

3

Jon Bentley Writing Efficient Programs è una grande fonte di tecniche di livello basso e alto - se riesci a trovare una copia.

2

rami Eliminare (se/qualcun'altro) utilizzando la matematica booleana:

if(x == 0) 
    x = 5; 

// becomes: 

x += (x == 0) * 5; 
// if '5' was a base 2 number, let's say 4: 
x += (x == 0) << 2; 

// divide by 2 if flag is set 
sum >>= (blendMode == BLEND); 

Questo velocizza veramente le cose, soprattutto quando questi IFS sono in un ciclo o da qualche parte che viene chiamato un sacco.

+0

Dubito che questa sarebbe un'ottimizzazione a livello di assemblaggio. Come descriveresti il ​​confronto e la moltiplicazione nel codice x86? – strager

+0

Forse questo compilatore è in grado di generare cmov solo per questi ultimi casi. – Joshua

1

Uso libero di __restrict per eliminare le bancarelle di carico-hit-store.

3

Quello da Assembler:

xor ax, ax 

invece di:

mov ax, 0 

ottimizzazione classica per le dimensioni e le prestazioni del programma.

4

Ottimizzazione della località cache, ad esempio quando si moltiplicano due matrici che non si adattano alla cache.

1

Rolling up loop.

Seriamente, l'ultima volta che avevo bisogno di fare qualcosa di simile era in una funzione che richiedeva l'80% del tempo di esecuzione, quindi valeva la pena provare a micro-ottimizzare se potessi ottenere un notevole aumento delle prestazioni.

La prima cosa che ho fatto è stato di arrotolare il ciclo. Questo mi ha dato un aumento di velocità molto significativo. Credo che questa fosse una questione di località cache.

La prossima cosa che ho fatto è stato aggiungere uno strato di riferimento indiretto e inserire un po 'di logica nel loop, che mi ha permesso di scorrere solo le cose di cui avevo bisogno. Questo non era un aumento di velocità, ma valeva la pena farlo.

Se si esegue un'ottimizzazione micro, è necessario avere un'idea ragionevole di due elementi: l'architettura che si sta effettivamente utilizzando (che è molto diversa dai sistemi con cui sono cresciuto, almeno per i micro- scopi di ottimizzazione) e cosa farà il compilatore per te.

Molte delle tradizionali micro-ottimizzazioni scambiano spazio per il tempo. Oggigiorno, usare più spazio aumenta le possibilità di perdere la cache, e ci sono le tue prestazioni. Inoltre, molti di questi sono ora realizzati da compilatori moderni, e in genere migliori di quanto sia probabile che li facciano.

Attualmente, è necessario (a) un profilo per vedere se è necessario eseguire l'ottimizzazione micro, e quindi (b) provare a scambiare il calcolo per lo spazio, nella speranza di conservare il più possibile nella cache. Infine, esegui alcuni test, così sai se hai migliorato le cose o le hai rovinate. I moderni compilatori e chip sono troppo complessi per poter mantenere un buon modello mentale e l'unico modo per sapere se l'ottimizzazione funziona o meno è testare.

1

Oltre al commento di Joshua sulla generazione di codice (una grande vittoria), e altri buoni suggerimenti, ...

non sono sicuro se si desidera chiamare "a basso livello", ma (e questo è downvote-bait) 1) stai lontano dall'usare altri livelli di astrazione di quanto assolutamente necessario, e 2) stai lontano dalla programmazione in stile notifica event-driven, se possibile.

  1. Se un computer esecuzione di un programma è come una macchina che corre una gara, una chiamata di metodo è come una deviazione. Questo non è necessariamente negativo, tranne che c'è una forte tentazione di annidare quelle cose, perché una volta che hai scritto una chiamata al metodo, tendi a dimenticare ciò che questa chiamata potrebbe costarti.

  2. Se ti affidi a eventi e notifiche, è perché hai più strutture dati che devono essere mantenute d'accordo. Questo è costoso e dovrebbe essere fatto solo se non puoi evitarlo.

Nella mia esperienza, i più grandi assassini di prestazioni sono troppa struttura dati e troppa astrazione.

1

Sono rimasto stupito l'aumento di velocità ho ottenuto sostituendo una per i numeri di loop sommando in struct:

const unsigned long SIZE = 100000000; 

typedef struct { 
    int a; 
    int b; 
    int result; 
} addition; 

addition *sum; 

void start() { 
    unsigned int byte_count = SIZE * sizeof(addition); 

    sum = malloc(byte_count); 
    unsigned int i = 0; 

    if (i < SIZE) { 
     do { 
      sum[i].a = i; 
      sum[i].b = i; 
      i++; 
     } while (i < SIZE); 
    }  
} 

void test_func() { 
    unsigned int i = 0; 

    if (i < SIZE) { // this is about 30% faster than the more obvious for loop, even with O3 
     do { 
      addition *s1 = &sum[i]; 
      s1->result = s1->b + s1->a; 
      i++; 
     } while (i<SIZE); 
    } 
} 

void finish() { 
    free(sum); 
} 

Perché non gcc ottimizzare i cicli for in questo? O c'è qualcosa che ho perso? Qualche effetto cache?

+0

Cosa intendi esattamente per "ciclo for" a cui stai confrontando questo? –

Problemi correlati