In C ++, dovrei preoccuparmi di memorizzare le variabili nella cache o lasciare che il compilatore faccia l'ottimizzazione? (Alias)


114

Considera il seguente codice ( pè di tipo unsigned char*ed bitmap->widthè di un tipo intero, che è esattamente sconosciuto e dipende dalla versione di alcune librerie esterne che stiamo utilizzando):

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

Vale la pena ottimizzarlo [..]

Potrebbe esserci un caso in cui ciò potrebbe produrre risultati più efficienti scrivendo:

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

... o è banale da ottimizzare per il compilatore?

Quale considereresti un codice "migliore"?

Nota dell'editore (Ike): per coloro che si interrogano sul testo barrato, la domanda originale, così come formulata, era pericolosamente vicina al territorio fuori tema ed era molto vicina alla chiusura nonostante il feedback positivo. Questi sono stati cancellati. Tuttavia, per favore, non punite coloro che hanno risposto a queste sezioni colpite della domanda.


19
Se *pè dello stesso tipo di widthallora non è banale da ottimizzare, poiché ppotrebbe puntarlo widthe modificarlo all'interno del ciclo.
emlai

31
Chiedere se il compilatore ottimizza una particolare operazione è di solito la domanda sbagliata. Quello che ti interessa (di solito) alla fine è quale versione funziona più velocemente, che dovresti semplicemente misurare.
SirGuy

4
@GuyGreer Sono d'accordo, anche se direi che la domanda è buona, o almeno interessante, purtroppo la risposta è "devi misurarla, per caso d'uso". Il motivo è che la funzionalità è portabile ma le prestazioni no. Quindi in realtà dipende da ogni parte del processo di compilazione, a partire dal compilatore e finire nel sito di destinazione (combinazione sistema operativo / hardware). E ovviamente la migliore ipotesi è che il compilatore sia più intelligente dell'essere umano in questo.
luk32

19
Se fossi un compilatore, vedrei che i tuoi due esempi non sono gli stessi. È possibile che ppunti alla stessa memoria di bitmap->width. Pertanto non posso ottimizzare legalmente il primo esempio per il secondo.
Mysticial

4
Dove viene memorizzata la "p"? Suggerirei che potresti ottenere una vincita di prestazioni davvero enorme facendo qualcosa come "char * limits p2 = p;" e quindi utilizzare "p2" invece di "p" all'interno del ciclo. Quindi, se desideri che le modifiche a "p2" vengano applicate nuovamente a p, usa "p + = (p2-p);". Nota che nessun puntatore scritto entro la vita di p2 da un puntatore non copiato da p2 può essere letto usando un puntatore copiato da p2, né viceversa, e nessuna copia di p2 può essere usata per alcuno scopo dopo la vita di p2, ma un compilatore può usarli fatti per consentire ottimizzazioni che non possono essere realizzate con altri mezzi.
supercat

Risposte:


81

A prima vista, pensavo che il compilatore potesse generare un assembly equivalente per entrambe le versioni con i flag di ottimizzazione attivati. Quando l'ho controllato, sono rimasto sorpreso di vedere il risultato:

fonte unoptimized.cpp

nota: questo codice non è pensato per essere eseguito.

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    for (unsigned x = 0 ; x < static_cast<unsigned>(bitmap.width) ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

fonte optimized.cpp

nota: questo codice non è pensato per essere eseguito.

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    const unsigned width = static_cast<unsigned>(bitmap.width);
    for (unsigned x = 0 ; x < width ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

Compilazione

  • $ g++ -s -O3 unoptimized.cpp
  • $ g++ -s -O3 optimized.cpp

Assembly (unoptimized.s)

    .file   "unoptimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    mov %eax, %edx
    addl    $1, %eax
    movq    (%rsi,%rdx,8), %rdx
    movb    $0, (%rdx)
    cmpl    bitmap(%rip), %eax
    jb  .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

Assemblaggio (ottimizzato.s)

    .file   "optimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    subl    $1, %eax
    leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    movq    (%rsi,%rax), %rdx
    addq    $8, %rax
    cmpq    %rcx, %rax
    movb    $0, (%rdx)
    jne .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

diff

$ diff -uN unoptimized.s optimized.s
--- unoptimized.s   2015-11-24 16:11:55.837922223 +0000
+++ optimized.s 2015-11-24 16:12:02.628922941 +0000
@@ -1,4 +1,4 @@
-   .file   "unoptimized.cpp"
+   .file   "optimized.cpp"
    .text
    .p2align 4,,15
 .globl main
@@ -10,16 +10,17 @@
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
+   subl    $1, %eax
+   leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
 .L3:
-   mov %eax, %edx
-   addl    $1, %eax
-   movq    (%rsi,%rdx,8), %rdx
+   movq    (%rsi,%rax), %rdx
+   addq    $8, %rax
+   cmpq    %rcx, %rax
    movb    $0, (%rdx)
-   cmpl    bitmap(%rip), %eax
-   jb  .L3
+   jne .L3
 .L2:
    xorl    %eax, %eax
    ret

L'assembly generato per la versione ottimizzata carica effettivamente ( lea) la widthcostante a differenza della versione non ottimizzata che calcola l' widthoffset ad ogni iterazione ( movq).

Quando avrò tempo, alla fine posterò qualche benchmark su questo. Buona domanda.


3
Sarebbe interessante vedere se il codice è stato generato in modo diverso se si esegue il cast a const unsignedinvece che solo unsignednel caso non ottimizzato.
Mark Ransom

2
@MarkRansom Immagino che non dovrebbe fare la differenza: la "promessa" di essere const è solo durante il singolo confronto, non per l'intero ciclo
Hagen von Eitzen

13
Si prega di NON utilizzare la funzione maindi prova per un'ottimizzazione. Gcc lo contrassegna intenzionalmente come freddo e quindi disabilita alcune ottimizzazioni per esso. Non so se questo sia il caso qui, ma è un'abitudine importante da prendere.
Marc Glisse

3
@ MarcGlisse Hai ragione al 100%. L'ho scritto in fretta, lo migliorerò.
YSC

3
Ecco un collegamento a entrambe le funzioni in un'unità di compilazione su godbolt , supponendo che bitmapsia globale. La versione non CSEd utilizza un operando di memoria per cmp, che non è un problema per perf in questo caso. Se fosse un locale, il compilatore potrebbe presumere che altri puntatori non possano "conoscerlo" e puntare ad esso. Non è una cattiva idea memorizzare espressioni che coinvolgono globali nelle variabili temporanee, a condizione che migliori (o non danneggi) la leggibilità o se le prestazioni sono critiche. A meno che non stiano succedendo molte cose, queste persone di solito possono semplicemente vivere nei registri e non essere mai versate.
Peter Cordes

38

In realtà non ci sono informazioni sufficienti dal tuo frammento di codice per poterlo dire e l'unica cosa a cui posso pensare è l'aliasing. Dal nostro punto di vista, è abbastanza chiaro che non vuoi pe bitmapche punti alla stessa posizione in memoria, ma il compilatore non lo sa e (poiché pè di tipo char*) il compilatore deve far funzionare questo codice anche se pe si bitmapsovrappongono.

Ciò significa in questo caso che se il ciclo cambia bitmap->widthattraverso il puntatore, pallora ciò deve essere visto durante la rilettura in bitmap->widthseguito, il che a sua volta significa che memorizzarlo in una variabile locale sarebbe illegale.

Detto questo, credo che alcuni compilatori a volte generino effettivamente due versioni dello stesso codice (ho visto prove circostanziali di ciò, ma non ho mai cercato direttamente informazioni su ciò che il compilatore sta facendo in questo caso) e controlla rapidamente se i puntatori alias ed eseguire il codice più veloce se determina che va bene.

Detto questo, resto fedele al mio commento sulla semplice misurazione delle prestazioni delle due versioni, i miei soldi sono nel non vedere alcuna differenza di prestazioni costante tra le due versioni del codice.

Secondo me, domande come queste vanno bene se il tuo scopo è imparare le teorie e le tecniche di ottimizzazione del compilatore, ma è una perdita di tempo (un'inutile microottimizzazione) se il tuo obiettivo finale qui è quello di rendere il programma più veloce.


1
@GuyGreer: è un importante blocco dell'ottimizzazione; Considero un peccato che le regole del linguaggio si concentrino su regole sui tipi efficaci, piuttosto che identificare le situazioni in cui le scritture e le letture di elementi diversi sono o non sono prive di sequenze. Le regole scritte in questo termine potrebbero fare un lavoro molto migliore nel soddisfare le esigenze del compilatore e del programmatore rispetto a quelle attuali.
supercat

3
@GuyGreer - un restrictqualificatore non sarebbe la risposta al problema di aliasing in questo caso?
LThode

4
Nella mia esperienza, restrictè in gran parte incostante. MSVC è l'unico compilatore che ho visto che sembra farlo correttamente. ICC perde le informazioni di aliasing tramite le chiamate di funzione anche se sono inline. E GCC di solito non ottiene alcun vantaggio a meno che non si dichiari ogni singolo parametro di input come restrict(incluso thisper le funzioni membro).
Mysticial

1
@Mystical: una cosa da ricordare è che charalias tutti i tipi, quindi se hai un carattere * devi usarlo restrictsu tutto. O se hai forzato le rigide regole di aliasing di GCC con, -fno-strict-aliasingallora tutto è considerato un possibile alias.
Zan Lynx

1
@Ray La proposta più recente per la restrictsemantica -like in C ++ è N4150 .
TC

24

Ok, ragazzi, quindi ho misurato, con GCC -O3(utilizzando GCC 4.9 su Linux x64).

Si scopre che la seconda versione funziona più velocemente del 54%!

Quindi, immagino che l'aliasing sia la cosa, non ci avevo pensato.

[Modificare]

Ho provato di nuovo la prima versione con tutti i puntatori definiti con __restrict__, ei risultati sono gli stessi. Strano ... o l'aliasing non è il problema, o, per qualche motivo, il compilatore non lo ottimizza bene anche con__restrict__ .

[Modifica 2]

Ok, penso di essere stato praticamente in grado di dimostrare che l'aliasing è il problema. Ho ripetuto il mio test originale, questa volta utilizzando un array anziché un puntatore:

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

E misurato (doveva usare "-mcmodel = large" per collegarlo). Poi ho provato:

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

I risultati della misurazione sono stati gli stessi: sembra che il compilatore sia stato in grado di ottimizzarlo da solo.

Poi ho provato i codici originali (con un puntatore p), questa volta quando pè di tipo std::uint16_t*. Anche in questo caso, i risultati sono stati gli stessi, a causa del rigoroso aliasing. Poi ho provato a costruire con "-fno-strict-aliasing", e di nuovo ho visto una differenza di tempo.


4
Sembra che dovrebbe essere un commento, anche se tecnicamente risponde alla domanda. Nota inoltre, sfortunatamente non hai dimostrato che l'aliasing fosse il problema. Sembra probabile, certamente plausibile, ma è diverso dal concludere che fosse così.
SirGuy

@GuyGreer: Vedi la mia [modifica 2] - ora penso che sia praticamente provato.
Yaron Cohen-Tal

2
Mi chiedo solo perché hai iniziato a usare la variabile "i" quando hai "x" nel tuo ciclo?
Jesper Madsen

1
Sono solo io che trovo la frase 54% più veloce difficile da comprendere? Vuoi dire che è 1,54 volte la velocità di quello non ottimizzato o qualcos'altro?
Roddy

3
@ YaronCohen-Tal quindi due volte più veloce? Impressionante, ma non quello che avrei pensato "54% più veloce" per significare!
Roddy

24

Altre risposte hanno sottolineato che sollevare l'operazione del puntatore fuori dal ciclo può cambiare il comportamento definito a causa di regole di aliasing che consentono a char di creare l'alias di qualsiasi cosa e quindi nonèun'ottimizzazione consentita per un compilatore anche se nella maggior parte dei casiè ovviamente corretto per un essere umano programmatore.

Hanno anche sottolineato che il sollevamento dell'operazione fuori dal circuito è solitamente, ma non sempre, un miglioramento dal punto di vista delle prestazioni ed è spesso negativo dal punto di vista della leggibilità.

Vorrei sottolineare che spesso esiste una "terza via". Invece di contare fino al numero di iterazioni che desideri, puoi contare fino a zero. Ciò significa che il numero di iterazioni è necessario solo una volta all'inizio del ciclo, non deve essere memorizzato dopo. Meglio ancora a livello di assemblatore spesso elimina la necessità di un confronto esplicito poiché l'operazione di decremento di solito imposta flag che indicano se il contatore era zero sia prima (flag di trasporto) che dopo (flag zero) il decremento.

for (unsigned x = static_cast<unsigned>(bitmap->width);x > 0;  x--)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

Notare che questa versione del ciclo fornisce valori x nell'intervallo 1..width invece che nell'intervallo 0 .. (width-1). Questo non ha importanza nel tuo caso perché in realtà non stai usando x per niente, ma è qualcosa di cui essere consapevoli. Se vuoi un ciclo di conto alla rovescia con valori x nell'intervallo 0 .. (larghezza-1) puoi farlo.

for (unsigned x = static_cast<unsigned>(bitmap->width); x-- > 0;)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

Puoi anche sbarazzarti dei cast negli esempi precedenti se vuoi senza preoccuparti del suo impatto sulle regole di confronto poiché tutto ciò che stai facendo con bitmap-> width è assegnarlo direttamente a una variabile.


2
Ho visto il secondo caso formattato come x --> 0, risultante nell'operatore "downto". Abbastanza divertente. PS Non considero una variabile per la condizione finale negativa per la leggibilità, in realtà può essere il contrario.
Mark Ransom

Dipende molto, a volte un'affermazione diventa così orribile che suddividerla in più affermazioni migliora la leggibilità, ma non credo che sia così qui.
plugwash

1
+1 Buona osservazione, anche se direi che sollevare static_cast<unsigned>(bitmap->width)e utilizzare widthinvece nel ciclo è in realtà un miglioramento per la leggibilità perché ora ci sono meno cose da analizzare per riga per il lettore. Tuttavia, le opinioni degli altri potrebbero differire.
SirGuy

1
Ci sono molte altre situazioni in cui il conteggio alla rovescia è migliore (ad esempio, quando si rimuovono elementi da un elenco). Non so perché questo non venga fatto più spesso.
Ian Goldby

3
Se vuoi scrivere loop che assomigliano di più all'asm ottimale, usa do { } while(), perché in ASM crei loop con un ramo condizionale alla fine. I cicli normali for(){}e while(){}richiedono istruzioni aggiuntive per testare la condizione del ciclo una volta prima del ciclo, se il compilatore non può provare che viene eseguito sempre almeno una volta. Con tutti i mezzi, usa for()o while()quando è utile per verificare se il ciclo deve essere eseguito anche una volta o quando è più leggibile.
Peter Cordes

11

L'unica cosa qui che può impedire l'ottimizzazione è la regola di aliasing rigorosa . In breve :

"Strict aliasing è un presupposto, fatto dal compilatore C (o C ++), che dereferenziare i puntatori a oggetti di tipi diversi non farà mai riferimento alla stessa posizione di memoria (cioè alias tra loro)".

[...]

L'eccezione alla regola è a char*, che può puntare a qualsiasi tipo.

L'eccezione si applica anche a unsignede signed charpuntatori.

Questo è il caso del tuo codice: stai modificando *ptramite pquale è un unsigned char*, quindi il compilatore deve presumere che possa puntare a bitmap->width. Quindi la memorizzazione nella cache di bitmap->widthè un'ottimizzazione non valida. Questo comportamento di prevenzione dell'ottimizzazione è mostrato nella risposta di YSC .

Se e solo se ppuntasse a un tipo non chare non decltype(bitmap->width), il caching sarebbe una possibile ottimizzazione.


10

La domanda originariamente posta:

Vale la pena ottimizzarlo?

E la mia risposta a questa domanda (ottenendo un buon mix di voti positivi e negativi ..)

Lascia che il compilatore se ne occupi.

Il compilatore quasi sicuramente farà un lavoro migliore di te. E non c'è alcuna garanzia che la tua "ottimizzazione" sia migliore del codice "ovvio": l'hai misurato ??

Ancora più importante, hai qualche prova che il codice che stai ottimizzando ha un impatto sulle prestazioni del tuo programma?

Nonostante i voti negativi (e ora vedendo il problema dell'aliasing), sono ancora contento che sia una risposta valida. Se non sai se vale la pena ottimizzare qualcosa, probabilmente non lo è.

Una domanda piuttosto diversa, ovviamente, sarebbe questa:

Come posso sapere se vale la pena ottimizzare un frammento di codice?

Innanzitutto, la tua applicazione o libreria deve essere eseguita più velocemente di quanto non faccia attualmente? L'utente ha continuato ad aspettare troppo a lungo? Il tuo software prevede il tempo di ieri invece di quello di domani?

Solo tu puoi davvero dirlo, in base allo scopo del tuo software e alle aspettative degli utenti.

Supponendo che il tuo software abbia bisogno di un po 'di ottimizzazione, la prossima cosa da fare è iniziare a misurare. I profiler ti diranno dove trascorre il tuo codice è tempo. Se il tuo frammento non viene visualizzato come un collo di bottiglia, è meglio lasciarlo stare. Anche i profiler e altri strumenti di misurazione ti diranno se le tue modifiche hanno fatto la differenza. È possibile passare ore a cercare di ottimizzare il codice, solo per scoprire che non hai fatto differenze evidenti.

Cosa intendi per "ottimizzazione", comunque?

Se non stai scrivendo codice "ottimizzato", allora il tuo codice dovrebbe essere il più chiaro, pulito e conciso possibile. L'argomento "L'ottimizzazione prematura è dannosa" non è una scusa per codice sciatto o inefficiente.

Il codice ottimizzato normalmente sacrifica alcuni degli attributi di cui sopra per le prestazioni. Potrebbe comportare l'introduzione di variabili locali aggiuntive, la presenza di oggetti con un ambito più ampio del previsto o persino l'inversione del normale ordinamento del ciclo. Tutti questi possono essere meno chiari o concisi, quindi documenta il codice (brevemente!) Sul motivo per cui lo stai facendo.

Ma spesso, con un codice "lento", queste micro-ottimizzazioni sono l'ultima risorsa. Il primo posto da considerare sono gli algoritmi e le strutture dati. C'è un modo per evitare del tutto il lavoro? Le ricerche lineari possono essere sostituite con quelle binarie? Un elenco collegato sarebbe più veloce qui di un vettore? O una tabella hash? Posso memorizzare i risultati nella cache? Prendere buone decisioni "efficienti" qui può spesso influire sulle prestazioni di un ordine di grandezza o più!


12
Quando si esegue l'iterazione sulla larghezza di un'immagine bitmap, la logica del ciclo può essere una parte significativa del tempo trascorso nel ciclo. Piuttosto che preoccuparsi di un'ottimizzazione prematura, in questo caso è meglio sviluppare best practice che siano efficienti fin dall'inizio.
Mark Ransom

4
@MarkRansom concorda, in parte: ma le "best practice" potrebbero essere: utilizzare una libreria esistente o una chiamata API per riempire le immagini, oppure b: ottenere che la GPU lo faccia per te. Non dovrebbe mai essere il tipo di microottimizzazione non misurata suggerita dall'OP. E come fai a sapere che questo codice viene mai eseguito più di una volta, o con bitmap più grandi di 16 pixel di larghezza ...?
Roddy

@Veedrac. Apprezzo la giustificazione per il -1. La spinta alla domanda è cambiata in modo sottile e sostanziale da quando ho risposto. Se pensi che la risposta (espansa) sia ancora inutile, è tempo per me di cancellarla ... "Vale la pena ..." è sempre principalmente basata sull'opinione, comunque.
Roddy

@Roddy Apprezzo le modifiche, aiutano (e il mio commento probabilmente suonava comunque troppo duro). Tuttavia, sono ancora sul recinto, poiché questa è davvero una risposta a una domanda che non è adatta per Stack Overflow. Sembra che una risposta adeguata sarebbe specifica per lo snippet, come le risposte altamente votate qui.
Veedrac

6

Uso il seguente schema in una situazione come questa. È quasi breve come il tuo primo caso ed è migliore del secondo, perché mantiene la variabile temporanea locale al ciclo.

for (unsigned int x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
{
  *p++ = 0xAA;
  *p++ = 0xBB;
  *p++ = 0xCC;
}

Questo sarà più veloce con un compilatore meno intelligente, build di debug o determinati flag di compilazione.

Edit1 : posizionare un'operazione costante al di fuori di un loop è una buona cosa modello di programmazione. Mostra la comprensione delle basi del funzionamento della macchina, specialmente in C / C ++. Direi che lo sforzo di mettersi alla prova dovrebbe essere sulle persone che non seguono questa pratica. Se il compilatore punisce per un buon pattern, è un bug nel compilatore.

Edit2:: Ho misurato il mio suggerimento rispetto al codice originale su vs2013, ho ottenuto un miglioramento di% 1. Possiamo fare di meglio? Una semplice ottimizzazione manuale offre un miglioramento 3 volte superiore al ciclo originale su macchine x64 senza ricorrere a istruzioni esotiche. Il codice seguente presuppone un sistema little endian e una bitmap correttamente allineata. TEST 0 è originale (9 sec), TEST 1 è più veloce (3 sec). Scommetto che qualcuno potrebbe renderlo ancora più veloce e il risultato del test dipenderà dalla dimensione della bitmap. Sicuramente presto in futuro, il compilatore sarà in grado di produrre codice costantemente più veloce. Temo che questo sarà il futuro in cui il compilatore sarà anche un programmatore AI, quindi saremmo senza lavoro. Ma per ora, scrivi semplicemente il codice che dimostri che sai che non sono necessarie operazioni extra nel ciclo.

#include <memory>
#include <time.h>

struct Bitmap_line
{
  int blah;
  unsigned int width;
  Bitmap_line(unsigned int w)
  {
    blah = 0;
    width = w;
  }
};

#define TEST 0 //define 1 for faster test

int main(int argc, char* argv[])
{
  unsigned int size = (4 * 1024 * 1024) / 3 * 3; //makes it divisible by 3
  unsigned char* pointer = (unsigned char*)malloc(size);
  memset(pointer, 0, size);
  std::unique_ptr<Bitmap_line> bitmap(new Bitmap_line(size / 3));
  clock_t told = clock();
#if TEST == 0
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
    //for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#else
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    unsigned x = 0;
    for (const unsigned n = static_cast<unsigned>(bitmap->width) - 4; x < n; x += 4)
    {
      *(int64_t*)p = 0xBBAACCBBAACCBBAALL;
      p += 8;
      *(int32_t*)p = 0xCCBBAACC;
      p += 4;
    }

    for (const unsigned n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#endif
  double ms = 1000.0 * double(clock() - told) / CLOCKS_PER_SEC;
  printf("time %0.3f\n", ms);

  {
    //verify
    unsigned char* p = pointer;
    for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      if ((*p++ != 0xAA) || (*p++ != 0xBB) || (*p++ != 0xCC))
      {
        printf("EEEEEEEEEEEEERRRRORRRR!!!\n");
        abort();
      }
    }
  }

  return 0;
}

Puoi risparmiare un altro 25% su 64 bit se usi tre int64_t invece di int64_t e int32_t.
Antonín Lejsek

5

Ci sono due cose da considerare.

A) Con che frequenza verrà eseguita l'ottimizzazione?

Se la risposta non è molto frequente, come solo quando un utente fa clic su un pulsante, non preoccuparti se rende il tuo codice illeggibile. Se la risposta è 1000 volte al secondo, probabilmente vorrai procedere con l'ottimizzazione. Se è anche un po 'complesso assicurati di inserire un commento per spiegare cosa sta succedendo per aiutare il prossimo ragazzo che arriva.

B) Questo renderà il codice più difficile da mantenere / risolvere i problemi?

Se non stai riscontrando un enorme guadagno in termini di prestazioni, rendere il tuo codice criptico semplicemente per risparmiare alcuni tick dell'orologio non è una buona idea. Molte persone ti diranno che ogni buon programmatore dovrebbe essere in grado di guardare il codice e capire cosa sta succedendo. Questo è vero. Il problema è che nel mondo degli affari il tempo extra per capirlo costa denaro. Quindi, se riesci a renderlo più carino da leggere, fallo. I tuoi amici ti ringrazieranno per questo.

Detto questo, userò personalmente l'esempio B.


4

Il compilatore è in grado di ottimizzare molte cose. Per il tuo esempio, dovresti scegliere la leggibilità, la manutenibilità e ciò che segue il tuo standard di codice. Per ulteriori informazioni su cosa può essere ottimizzato (con GCC), vedere questo post del blog .


4

Come regola generale, lascia che il compilatore esegua l'ottimizzazione per te, finché non decidi che dovresti subentrare. La logica per questo non ha nulla a che fare con le prestazioni, ma piuttosto con la leggibilità umana. Nella stragrande maggioranza dei casi, la leggibilità del programma è più importante delle sue prestazioni. Dovresti mirare a scrivere codice più facile da leggere per un essere umano, e poi preoccuparti dell'ottimizzazione solo quando sei convinto che le prestazioni siano più importanti della manutenibilità del tuo codice.

Una volta notato che le prestazioni sono importanti, è necessario eseguire un profiler sul codice per determinare quali loop sono inefficienti e ottimizzarli individualmente. Potrebbero effettivamente esserci casi in cui si desidera eseguire tale ottimizzazione (soprattutto se si migra verso C ++, dove vengono coinvolti i contenitori STL), ma il costo in termini di leggibilità è ottimo.

Inoltre, posso pensare a situazioni patologiche in cui potrebbe effettivamente rallentare il codice. Ad esempio, si consideri il caso in cui il compilatore non è stato in grado di dimostrare che bitmap->widthera costante durante il processo. Aggiungendo la widthvariabile si forza il compilatore a mantenere una variabile locale in tale ambito. Se, per qualche motivo specifico della piattaforma, quella variabile aggiuntiva ha impedito un'ottimizzazione dello spazio dello stack, potrebbe essere necessario riorganizzare il modo in cui emette i bytecode e produrre qualcosa di meno efficiente.

Ad esempio, su Windows x64, si è obbligati a chiamare una chiamata API speciale, __chkstknel preambolo della funzione se la funzione utilizzerà più di 1 pagina di variabili locali. Questa funzione offre a Windows la possibilità di gestire le pagine di guardia che utilizzano per espandere lo stack quando necessario. Se la tua variabile aggiuntiva spinge l'utilizzo dello stack da sotto 1 pagina a 1 pagina o sopra, la tua funzione è ora obbligata a chiamare __chkstkogni volta che viene inserita. Se dovessi ottimizzare questo ciclo su un percorso lento, potresti effettivamente rallentare il percorso veloce più di quanto hai salvato sul percorso lento!

Certo, è un po 'patologico, ma il punto di quell'esempio è che puoi effettivamente rallentare il compilatore. Mostra solo che devi profilare il tuo lavoro per determinare dove vanno le ottimizzazioni. Nel frattempo, non sacrificare in alcun modo la leggibilità per un'ottimizzazione che può o non può avere importanza.


4
Vorrei che C e C ++ fornissero più modi per identificare esplicitamente le cose a cui il programmatore non interessa. Non solo fornirebbero più possibilità ai compilatori di ottimizzare le cose, ma eviterebbero ad altri programmatori che leggono il codice di dover indovinare se ad es. Potrebbe ricontrollare bitmap-> larghezza ogni volta per assicurarsi che le modifiche ad esso influenzino il ciclo, o se potrebbe essere la cache bitmap-> larghezza per garantire che le modifiche ad essa non influenzino il ciclo. Avere un mezzo per dire "Memorizza nella cache questo o no, non mi interessa" renderebbe chiaro il motivo della scelta del programmatore.
supercat

@supercat Sono d'accordo di tutto cuore, come si può vedere se si guardano le pile di linguaggi tatuati falliti che ho cercato di scrivere per risolvere questo problema. Ho scoperto che è notevolmente difficile definire "cosa" a qualcuno non interessa senza una sintassi così empia che semplicemente non ne vale la pena. Continuo la mia ricerca invano.
Cort Ammon

Non è possibile definirlo in tutti i casi, ma penso che ci siano molti casi in cui il sistema di tipi potrebbe aiutare. È troppo C ha deciso di rendere i tipi di carattere "accesso universale" piuttosto che avere un qualificatore di tipo che era un po 'più libero di "volatile" che potrebbe essere applicato a qualsiasi tipo, con la semantica che gli accessi di tali tipi sarebbero elaborati in sequenza con accessi del tipo equivalente non qualificato e anche con accessi di tutti i tipi di variabili con lo stesso qualificatore. Ciò aiuterebbe a chiarire se si utilizzavano i tipi di carattere perché era necessario il ...
supercat

... comportamento di aliasing, o se li si stava usando perché erano della misura giusta per soddisfare le proprie esigenze. Sarebbe anche utile disporre di barriere di aliasing esplosive che in molti casi potrebbero essere collocate al di fuori dei cicli, a differenza delle barriere implicite associate agli accessi di tipo carattere.
supercat

1
Questo è un discorso saggio, ma, in genere, se selezioni già C per il tuo compito, probabilmente la prestazione è molto importante e dovrebbero essere applicate le diverse regole. Altrimenti potrebbe essere meglio usare Ruby, Java, Python o qualcosa di simile.
Audrius Meskauskas

4

Il confronto è sbagliato poiché i due frammenti di codice

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)

e

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x<width ;  ++x)

non sono equivalenti

Nel primo caso widthè dipendente e non costante e non si può presumere che non possa cambiare tra le iterazioni successive. Quindi non può essere ottimizzato, ma deve essere controllato ad ogni ciclo .

Nel tuo caso ottimizzato, a una variabile locale viene assegnato il valore di bitmap->widthad un certo punto durante l'esecuzione del programma. Il compilatore può verificare che ciò non cambi effettivamente.

Hai pensato al multi threading, o forse il valore potrebbe essere dipendente dall'esterno in modo tale che il suo valore sia volatile. Come ci si aspetterebbe che il compilatore capisca tutte queste cose se non lo dici?

Il compilatore può fare solo quanto il tuo codice lo consente.


2

A meno che tu non sappia esattamente come il compilatore ottimizza il codice, è meglio fare le tue ottimizzazioni mantenendo la leggibilità del codice e il design. In pratica è difficile controllare il codice assembly per ogni funzione che scriviamo per le nuove versioni del compilatore.


1

Il compilatore non può ottimizzare bitmap->widthperché il valore di widthpuò essere modificato tra le iterazioni. Ci sono alcuni motivi più comuni:

  1. Multi-threading. Il compilatore non può prevedere se un altro thread sta per modificare il valore.
  2. Modifica all'interno del ciclo, a volte non è semplice dire se la variabile verrà cambiata all'interno del ciclo.
  3. E 'chiamata di funzione, ad esempio, iterator::end()o container::size()per cui è difficile prevedere se sarà sempre restituisce lo stesso risultato.

Per riassumere (la mia opinione personale) per luoghi che richiedono un alto livello di ottimizzazione è necessario farlo da soli, in altri posti basta lasciarlo, il compilatore può ottimizzarlo o meno, se non ci sono grandi differenze la leggibilità del codice è l'obiettivo principale.

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.