Il livello di ottimizzazione -O3 è pericoloso in g ++?


233

Ho sentito da varie fonti (anche se principalmente da un mio collega) che la compilazione con un livello di ottimizzazione di -O3in g ++ è in qualche modo "pericolosa" e dovrebbe essere evitata in generale a meno che non sia dimostrata necessaria.

È vero, e se sì, perché? Dovrei solo attenermi -O2?


38
È pericoloso solo se fai affidamento su un comportamento indefinito. E anche allora sarei sorpreso se fosse il livello di ottimizzazione a rovinare qualcosa.
Seth Carnegie,

5
Il compilatore è ancora costretto a produrre un programma che si comporta "come se" compilasse esattamente il codice. Non so che -O3sia considerato particolarmente difettoso? Penso che forse possa peggiorare il comportamento indefinito in quanto potrebbe fare cose strane e meravigliose basate su determinati presupposti, ma sarebbe colpa tua. Quindi, in generale, direi che va bene.
BoBTFish,

5
È vero che livelli di ottimizzazione più elevati sono più inclini ai bug del compilatore. Ho riscontrato alcuni casi, ma in generale sono ancora piuttosto rari.
Mistico il

21
-O2si accende -fstrict-aliasinge se il tuo codice sopravvive a quello, probabilmente sopravviverà ad altre ottimizzazioni, dal momento che è quello che le persone sbagliano ancora e ancora. Detto questo, -fpredictive-commoningè solo disponibile -O3e l'abilitazione potrebbe abilitare i bug nel codice causati da ipotesi errate sulla concorrenza. Meno sbagliato è il codice, meno pericolosa è l'ottimizzazione ;-)
Steve Jessop,

6
@PlasmaHH, non credo che "più rigoroso" sia una buona descrizione di -Ofast, ad esempio, disattiva la gestione delle NaN conforme all'IEEE
Jonathan Wakely,

Risposte:


223

Agli inizi di gcc (2.8 ecc.) E ai tempi di egcs, e redhat 2.96 -O3 a volte era piuttosto difettoso. Ma questo è successo più di un decennio fa e -O3 non è molto diverso da altri livelli di ottimizzazione (in buggy).

Tende tuttavia a rivelare casi in cui le persone fanno affidamento su comportamenti indefiniti, a causa del fatto di affidarsi più strettamente alle regole, e in particolare ai casi angolari, delle lingue.

Come nota personale, sto eseguendo software di produzione nel settore finanziario da molti anni ormai con -O3 e non ho ancora riscontrato un bug che non sarebbe stato lì se avessi usato -O2.

A grande richiesta, ecco un'aggiunta:

-O3 e in particolare flag aggiuntivi come -funroll-loops (non abilitato da -O3) a volte possono generare più codice macchina. In determinate circostanze (ad es. Su una CPU con cache di istruzioni L1 eccezionalmente piccola), ciò può causare un rallentamento a causa di tutto il codice, ad esempio di un ciclo interno, che ora non si adatta più a L1I. Generalmente gcc si impegna piuttosto a non generare così tanto codice, ma poiché di solito ottimizza il caso generico, ciò può accadere. Le opzioni particolarmente inclini a questo (come lo srotolamento del loop) normalmente non sono incluse in -O3 e sono contrassegnate di conseguenza nella manpage. Come tale, è generalmente una buona idea usare -O3 per generare codice veloce e ricorrere a -O2 o -Os (che tenta di ottimizzare la dimensione del codice) quando appropriato (ad es. Quando un profiler indica errori L1I).

Se vuoi portare l'ottimizzazione all'estremo, puoi modificare in gcc tramite --param i costi associati a determinate ottimizzazioni. Inoltre nota che gcc ora ha la capacità di mettere attributi su funzioni che controllano le impostazioni di ottimizzazione solo per queste funzioni, quindi quando trovi che hai un problema con -O3 in una funzione (o vuoi provare flag speciali solo per quella funzione), non è necessario compilare l'intero file o l'intero progetto con O2.

sembra che si debba fare attenzione quando si usa -Ofast, che afferma:

-Ofast abilita tutte le ottimizzazioni -O3. Consente inoltre ottimizzazioni non valide per tutti i programmi conformi standard.

il che mi porta a concludere che -O3 deve essere completamente conforme agli standard.


2
Uso solo qualcosa di simile al contrario. Uso sempre -Os o -O2 (a volte O2 genera un eseguibile più piccolo) .. dopo la profilazione utilizzo O3 su parti del codice che richiedono più tempo di esecuzione e che da sole può dare fino al 20% di velocità in più.
CoffeDeveloper

3
Lo faccio per la velocità. O3 la maggior parte delle volte rende le cose più lente. Non so esattamente perché, sospetto che inquini la cache delle istruzioni.
CoffeDeveloper

4
@DarioOO Ho voglia di supplicare che "code bloat" sia una cosa popolare da fare, ma non lo vedo quasi mai supportato da benchmark. Dipende molto dall'architettura, ma ogni volta che vedo benchmark pubblicati (ad esempio phoronix.com/… ) mostra che O3 è più veloce nella stragrande maggioranza dei casi. Ho visto la profilazione e un'attenta analisi necessarie per dimostrare che il bloat del codice era in realtà un problema, e di solito accade solo per le persone che abbracciano i modelli in modo estremo.
Nir Friedman,

1
@NirFriedman: tende ad avere un problema quando il modello di costo integrato del compilatore presenta dei bug o quando si ottimizza per un target completamente diverso da quello in cui si esegue. Stranamente, questo vale per tutti i livelli di ottimizzazione ...
PlasmaHH,

1
@PlasmaHH: il problema using-cmov sarebbe difficile da risolvere per il caso generale. Di solito non hai semplicemente ordinato i tuoi dati, quindi quando gcc sta cercando di decidere se un ramo è prevedibile o meno, std::sortè improbabile che l' analisi statica che cerchi chiamate a funzioni. Usare qualcosa come stackoverflow.com/questions/109710/… sarebbe di aiuto, o forse scrivere la fonte per trarre vantaggio dall'ordinamento: scansionare fino a vedere> = 128, quindi iniziare a sommare. Per quanto riguarda il codice gonfio, sì, ho intenzione di andare in giro a segnalarlo. : P
Peter Cordes,

42

Nella mia esperienza in qualche modo a scacchi, l'applicazione -O3a un intero programma rende quasi sempre più lenta (rispetto a -O2), perché attiva lo srotolamento e l'inserimento di loop aggressivi che rendono il programma non più adatto alla cache delle istruzioni. Per programmi più grandi, questo può essere vero anche per quanto -O2riguarda -Os!

Lo schema d'uso previsto -O3è che, dopo aver creato il profilo del programma, lo si applica manualmente a una manciata di file contenenti loop interni critici che traggono effettivamente vantaggio da questi compromessi aggressivi spazio-velocità. Le versioni più recenti di GCC hanno una modalità di ottimizzazione guidata dal profilo che può (IIUC) applicare selettivamente le -O3ottimizzazioni alle funzioni a caldo, automatizzando efficacemente questo processo.


10
"quasi sempre"? Rendilo "50-50", e avremo un accordo ;-).
No-Bugs Hare,

12

L'opzione -O3 attiva ottimizzazioni più costose, come l'integrazione delle funzioni, oltre a tutte le ottimizzazioni dei livelli inferiori "-O2" e "-O1". Il livello di ottimizzazione '-O3' può aumentare la velocità dell'eseguibile risultante, ma può anche aumentare le sue dimensioni. In alcune circostanze in cui queste ottimizzazioni non sono favorevoli, questa opzione potrebbe effettivamente rallentare un programma.


3
Comprendo che alcune "ottimizzazioni apparenti" potrebbero rallentare un programma, ma hai una fonte che afferma che GCC-O3 ha rallentato un programma?
Mooing Duck,

1
@MooingDuck: Anche se non riesco a citare una fonte, ricordo di essermi imbattuto in un caso del genere con alcuni vecchi processori AMD che avevano una cache L1I piuttosto piccola (~ 10k istruzioni). Sono sicuro che Google ha di più per gli interessati, ma soprattutto opzioni come lo svolgimento di loop non fanno parte di O3 e aumentano molto le dimensioni. -Os è quello per quando vuoi rendere il file eseguibile più piccolo. Anche -O2 può aumentare la dimensione del codice. Un ottimo strumento per giocare con il risultato di diversi livelli di ottimizzazione è Gcc Explorer.
PlasmaHH,

@PlasmaHH: In realtà, una piccola dimensione della cache è qualcosa che un compilatore potrebbe rovinare, buon punto. Questo è davvero un buon esempio. Per favore, inseriscilo nella risposta.
Mooing Duck,

1
@PlasmaHH Pentium III aveva una cache di codice di 16 KB. Il K6 di AMD e superiori in realtà aveva una cache di istruzioni di 32 KB. P4 è iniziato con circa 96 KB di valore. Il Core I7 in realtà ha una cache di codice L1 da 32 KB. I decodificatori di istruzioni sono potenti al giorno d'oggi, quindi il tuo L3 è abbastanza buono da ripiegare su quasi tutti i loop.
doug65536,

1
Vedrai un enorme aumento delle prestazioni ogni volta che c'è una funzione chiamata in un ciclo e può fare una significativa eliminazione della sottoespressione comune e sollevare il ricalcolo non necessario dalla funzione a prima del ciclo.
doug65536,

8

Sì, O3 è più forte. Sono uno sviluppatore di compilatori e ho identificato chiari e ovvi bug gcc causati da O3 che generava istruzioni di assemblaggio SIMD errate durante la creazione del mio software. Da quello che ho visto, la maggior parte dei software di produzione viene fornita con O2, il che significa che O3 riceverà meno attenzione rispetto ai test e alle correzioni di bug.

Pensala in questo modo: O3 aggiunge più trasformazioni sopra O2, che aggiunge più trasformazioni sopra O1. Statisticamente parlando, più trasformazioni significano più bug. Questo è vero per qualsiasi compilatore.


3

Di recente ho riscontrato un problema con l'ottimizzazione con g++. Il problema era legato a una scheda PCI, in cui i registri (per comando e dati) erano rappresentati da un indirizzo di memoria. Il mio driver ha mappato l'indirizzo fisico su un puntatore all'interno dell'applicazione e lo ha dato al processo chiamato, che ha funzionato con esso in questo modo:

unsigned int * pciMemory;
askDriverForMapping( & pciMemory );
...
pciMemory[ 0 ] = someCommandIdx;
pciMemory[ 0 ] = someCommandLength;
for ( int i = 0; i < sizeof( someCommand ); i++ )
    pciMemory[ 0 ] = someCommand[ i ];

La carta non ha funzionato come previsto. Quando ho visto l'assemblea ho capito che il compilatore ha scritto solo someCommand[ the last ]in pciMemory, tralasciando tutte le scritture precedenti.

In conclusione: essere accurati e attenti con l'ottimizzazione.


38
Ma il punto qui è che il tuo programma ha semplicemente un comportamento indefinito; l'ottimizzatore non ha fatto nulla di male. In particolare è necessario dichiarare pciMemorycome volatile.
Konrad Rudolph,

11
In realtà non è UB ma il compilatore ha il diritto di omettere tutto tranne l'ultimo, pciMemoryperché tutte le altre scritture non hanno alcun effetto. Per l'ottimizzatore è fantastico perché può rimuovere molte istruzioni inutili e che richiedono tempo.
Konrad Rudolph,

4
L'ho trovato in standard (dopo oltre 10 anni))) - Una dichiarazione volatile può essere usata per descrivere un oggetto corrispondente a una porta di input / output mappata in memoria o un oggetto a cui accede da una funzione di interruzione asincrona. Le azioni su oggetti così dichiarati non devono essere "ottimizzate" da un'implementazione o riordinate ad eccezione di quanto consentito dalle regole per la valutazione delle espressioni.
borisbn

2
@borisbn Un po 'fuori tema ma come fai a sapere che il tuo dispositivo ha preso il comando prima di inviare un nuovo comando?
user877329

3
@ user877329 L'ho visto dal comportamento del dispositivo, ma è stata una grande ricerca
borisbn
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.