Perché GCC non ottimizza a * a * a * a * a * a a (a * a * a) * (a * a * a)?


2120

Sto facendo qualche ottimizzazione numerica su un'applicazione scientifica. Una cosa che ho notato è che GCC ottimizzerà la chiamata pow(a,2)compilandola a*a, ma la chiamata pow(a,6)non è ottimizzata e chiamerà effettivamente la funzione di libreria pow, che rallenta notevolmente le prestazioni. (Al contrario, il compilatore Intel C ++ , eseguibile icc, eliminerà la richiesta della libreria pow(a,6).)

Ciò di cui sono curioso è che quando ho sostituito pow(a,6)con l' a*a*a*a*a*autilizzo di GCC 4.5.1 e le opzioni " -O3 -lm -funroll-loops -msse4", utilizza 5 mulsdistruzioni:

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13

mentre se scrivo (a*a*a)*(a*a*a), produrrà

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13

che riduce il numero di istruzioni di moltiplicazione a 3. iccha un comportamento simile.

Perché i compilatori non riconoscono questo trucco di ottimizzazione?


13
Che cosa significa "riconoscere pow (a, 6)"?
Varun Madiath,

659
Um ... sai che a a a a a a e (a a a) * (a a * a) non sono gli stessi con i numeri in virgola mobile, vero? Dovrai usare -funsafe-math o -ffast-math o qualcosa del genere.
Damon,

106
Ti consiglio di leggere "Ciò che ogni scienziato informatico dovrebbe sapere sull'aritmetica in virgola mobile" di David Goldberg: download.oracle.com/docs/cd/E19957-01/806-3568/… dopo di che avrai una comprensione più completa di la fossa di catrame in cui sei appena entrato!
Phil Armstrong,

189
Una domanda perfettamente ragionevole. 20 anni fa ho posto la stessa domanda generale e, schiacciando quel singolo collo di bottiglia, ho ridotto il tempo di esecuzione di una simulazione Monte Carlo da 21 ore a 7 ore. Il codice nel ciclo interno è stato eseguito 13 trilioni di volte nel processo, ma ha portato la simulazione in una finestra notturna. (vedi risposta sotto)

23
Forse anche buttare (a*a)*(a*a)*(a*a)nel mix. Stesso numero di moltiplicazioni, ma probabilmente più preciso.
Rok Kralj,

Risposte:


2738

Perché la matematica a virgola mobile non è associativa . Il modo in cui raggruppate gli operandi nella moltiplicazione in virgola mobile ha un effetto sull'accuratezza numerica della risposta.

Di conseguenza, la maggior parte dei compilatori è molto prudente riguardo al riordino dei calcoli in virgola mobile a meno che non possano essere sicuri che la risposta rimanga invariata o a meno che non dite loro che non vi interessa l'accuratezza numerica. Ad esempio: l' -fassociative-mathopzione di gcc che consente a gcc di riassociare le operazioni in virgola mobile, o anche l' -ffast-mathopzione che consente compromessi ancora più aggressivi di precisione rispetto alla velocità.


10
Sì. Con -ffast-math sta facendo tale ottimizzazione. Buona idea! Ma poiché il nostro codice riguarda una maggiore precisione rispetto alla velocità, potrebbe essere meglio non passarlo.
XX

19
IIRC C99 consente al compilatore di eseguire tali ottimizzazioni FP "non sicure", ma GCC (su qualsiasi cosa diversa da x87) fa un ragionevole tentativo di seguire IEEE 754 - non è "limiti di errore"; c'è solo una risposta corretta .
tc.

14
I dettagli di implementazione di pownon sono né qui né lì; questa risposta non fa nemmeno riferimento pow.
Stephen Canon,

14
@nedR: l'impostazione predefinita di ICC è quella di consentire la riassociazione. Se si desidera ottenere un comportamento conforme agli standard, è necessario impostare -fp-model precisecon ICC. clange gccinadempienza a rigorosa conformità e riassociazione.
Stephen Canon,

49
@xis, non è davvero che -fassociative-mathsarebbe impreciso; è solo questo a*a*a*a*a*ae (a*a*a)*(a*a*a)sono diversi. Non si tratta di precisione; si tratta di conformità agli standard e risultati rigorosamente ripetibili, ad esempio gli stessi risultati su qualsiasi compilatore. I numeri in virgola mobile non sono già esatti. Raramente è inappropriato compilare -fassociative-math.
Paul Draper,

652

Lambdageek sottolinea correttamente che, poiché l'associatività non vale per i numeri in virgola mobile, l '"ottimizzazione" dia*a*a*a*a*ato(a*a*a)*(a*a*a)può modificare il valore. Questo è il motivo per cui è vietato da C99 (a meno che non sia espressamente consentito dall'utente, tramite flag di compilazione o pragma). Generalmente, il presupposto è che il programmatore abbia scritto ciò che ha fatto per una ragione, e il compilatore dovrebbe rispettarlo. Se vuoi(a*a*a)*(a*a*a), scrivilo.

Questo può essere un dolore da scrivere, però; perché il compilatore non può semplicemente fare ciò che consideri giusto quando lo usi pow(a,6)? Perché sarebbe la cosa sbagliata da fare. Su una piattaforma con una buona libreria matematica, pow(a,6)è significativamente più preciso di uno a*a*a*a*a*ao (a*a*a)*(a*a*a). Solo per fornire alcuni dati, ho eseguito un piccolo esperimento sul mio Mac Pro, misurando l'errore peggiore nel valutare un ^ 6 per tutti i numeri fluttuanti a precisione singola tra [1,2):

worst relative error using    powf(a, 6.f): 5.96e-08
worst relative error using (a*a*a)*(a*a*a): 2.94e-07
worst relative error using     a*a*a*a*a*a: 2.58e-07

L'uso powinvece di un albero di moltiplicazione riduce l'errore associato di un fattore 4 . I compilatori non dovrebbero (e in genere non lo fanno) fare "ottimizzazioni" che aumentano l'errore a meno che non siano autorizzati dall'utente a farlo (ad es. Via -ffast-math).

Si noti che GCC fornisce __builtin_powi(x,n)un'alternativa a pow( ), che dovrebbe generare un albero di moltiplicazione in linea. Usalo se vuoi compromettere la precisione per le prestazioni, ma non vuoi abilitare la matematica veloce.


29
Si noti inoltre che Visual C ++ fornisce una versione "avanzata" di pow (). Chiamando _set_SSE2_enable(<flag>)con flag=1, utilizzerà SSE2 se possibile. Ciò riduce la precisione di un po ', ma migliora la velocità (in alcuni casi). MSDN: _set_SSE2_enable () e pow ()
TkTech

18
@TkTech: qualsiasi precisione ridotta è dovuta all'implementazione di Microsoft, non alla dimensione dei registri utilizzati. È possibile fornire un arrotondato correttamente pow utilizzando solo registri a 32 bit, se lo scrittore della libreria è così motivato. Ci sono powimplementazioni basate su SSE che sono più accurate della maggior parte delle implementazioni basate su x87 e ci sono anche implementazioni che compromettono la precisione per la velocità.
Stephen Canon,

9
@TkTech: Certo, volevo solo chiarire che la riduzione dell'accuratezza è dovuta alle scelte fatte dagli autori delle biblioteche, non intrinseche all'uso di SSE.
Stephen Canon,

7
Sono interessato a sapere cosa hai usato come "gold standard" qui per il calcolo degli errori relativi - normalmente mi sarei aspettato che lo fosse a*a*a*a*a*a, ma a quanto pare non è così! :)
j_random_hacker,

8
@j_random_hacker: dal momento che stavo confrontando i risultati a precisione singola, la doppia precisione è sufficiente per un gold standard - l'errore da a a a a a calcolato in doppio è * notevolmente inferiore all'errore di uno qualsiasi dei calcoli a precisione singola.
Stephen Canon,

168

Un altro caso simile: maggior parte dei compilatori non sarà ottimizzare a + b + c + da (a + b) + (c + d)(questo è un'ottimizzazione a partire dalla seconda espressione può essere pipeline meglio) e valutare come data (cioè come (((a + b) + c) + d)). Anche questo è dovuto a casi angolari:

float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));

Questo produce 1.000000e-05 0.000000e+00


10
Questo non è esattamente lo stesso. Modificare l'ordine delle moltiplicazioni / divisioni (esclusa la divisione per 0) è più sicuro dell'ordine del cambiamento di somma / sottrazione. Secondo la mia modesta opinione, il compilatore dovrebbe provare ad associare mults./divs. perché così facendo si riduce il numero totale di operazioni e oltre al guadagno prestazionale c'è anche un guadagno di precisione.
CoffeDeveloper

4
@DarioOO: non è più sicuro. Moltiplicare e dividere sono gli stessi dell'addizione e della sottrazione dell'esponente e la modifica dell'ordine può facilmente far sì che i temporali superino l'intervallo possibile dell'esponente. (Non esattamente lo stesso, perché l'esponente non subisce perdita di precisione ... ma la rappresentazione è ancora piuttosto limitata e il riordino può portare a valori non rappresentabili)
Ben Voigt

8
Penso che ti manchi un po 'di background di calcolo. Moltiplicare e dividere 2 numeri introduce la stessa quantità di errore. Mentre sottrarre / aggiungere 2 numeri può introdurre un errore più grande soprattutto quando i 2 numeri sono di ordine di grandezza diverso, quindi è più sicuro riordinare mul / divide di sub / aggiungere perché introduce un piccolo cambiamento nell'errore finale.
CoffeDeveloper

8
@DarioOO: il rischio è diverso con mul / div: il riordino fa una modifica trascurabile nel risultato finale, o l'esponente trabocca ad un certo punto (dove non avrebbe mai avuto prima) e il risultato è enormemente diverso (potenzialmente + inf o 0).
Peter Cordes,

@GameDeveloper Imporre un guadagno di precisione in modi imprevedibili è estremamente problematico.
curiousguy,

80

Fortran (progettato per il calcolo scientifico) ha un operatore di potenza integrato e, per quanto ne so, i compilatori di Fortran ottimizzano comunemente l'innalzamento a potenze intere in modo simile a quello che descrivi. C / C ++ purtroppo non ha un operatore di potenza, solo la funzione di libreria pow(). Ciò non impedisce ai compilatori intelligenti di trattare in modo powspeciale e di elaborarlo in modo più rapido per casi speciali, ma sembra che lo facciano meno comunemente ...

Alcuni anni fa stavo cercando di rendere più conveniente il calcolo di potenze intere in modo ottimale, e ho trovato quanto segue. È C ++, non C, e dipende ancora dal fatto che il compilatore sia in qualche modo intelligente su come ottimizzare / incorporare le cose. Comunque, spero che potresti trovarlo utile in pratica:

template<unsigned N> struct power_impl;

template<unsigned N> struct power_impl {
    template<typename T>
    static T calc(const T &x) {
        if (N%2 == 0)
            return power_impl<N/2>::calc(x*x);
        else if (N%3 == 0)
            return power_impl<N/3>::calc(x*x*x);
        return power_impl<N-1>::calc(x)*x;
    }
};

template<> struct power_impl<0> {
    template<typename T>
    static T calc(const T &) { return 1; }
};

template<unsigned N, typename T>
inline T power(const T &x) {
    return power_impl<N>::calc(x);
}

Chiarimento per i curiosi: questo non trova il modo ottimale per calcolare i poteri, ma dal momento che trovare la soluzione ottimale è un problema NP-completo e questo vale la pena fare comunque solo per piccoli poteri (al contrario dell'uso pow), non c'è motivo di agitarsi con il dettaglio.

Quindi usalo come power<6>(a).

Ciò semplifica la digitazione dei poteri (non è necessario precisare 6 as con parentesi) e consente di avere questo tipo di ottimizzazione senza -ffast-mathnel caso in cui si abbia qualcosa che dipende dalla precisione come la somma compensata (un esempio in cui l'ordine delle operazioni è essenziale) .

Probabilmente puoi anche dimenticare che questo è C ++ e usarlo nel programma C (se si compila con un compilatore C ++).

Spero che questo possa essere utile.

MODIFICARE:

Questo è ciò che ottengo dal mio compilatore:

Per a*a*a*a*a*a,

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0

Per (a*a*a)*(a*a*a),

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm0, %xmm0

Per power<6>(a),

    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm0, %xmm1

36
Trovare l'albero di potenza ottimale potrebbe essere difficile, ma dal momento che è interessante solo per piccoli poteri, la risposta ovvia è pre-calcolarlo una volta (Knuth fornisce una tabella fino a 100) e usare quella tabella hardcoded (questo è ciò che gcc fa internamente per Powi) .
Marc Glisse,

7
Sui processori moderni, la velocità è limitata dalla latenza. Ad esempio, il risultato di una moltiplicazione potrebbe essere disponibile dopo cinque cicli. In quella situazione, trovare il modo più veloce per creare un po 'di potere potrebbe essere più complicato.
gnasher729,

3
Potresti anche provare a trovare l'albero di potenza che fornisce il limite superiore più basso per l'errore di arrotondamento relativo o l'errore di arrotondamento relativo medio più basso.
gnasher729,

1
Boost ha anche il supporto per questo, ad esempio boost :: math :: pow <6> (n); Penso che provi persino a ridurre il numero di moltiplicazioni estraendo fattori comuni.
gast128,

Si noti che l'ultimo è equivalente a (a ** 2) ** 3
minmaxavg

62

GCC realtà non ottimizzare a*a*a*a*a*aper (a*a*a)*(a*a*a)quando a è un numero intero. Ho provato con questo comando:

$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -

Ci sono molte bandiere gcc ma niente di speciale. Significano: leggi da stdin; utilizzare il livello di ottimizzazione O2; elenco delle lingue dell'assembly di output anziché binario; l'elenco deve utilizzare la sintassi del linguaggio assembly Intel; l'input è in linguaggio C (di solito il linguaggio viene dedotto dall'estensione del file di input, ma non c'è estensione del file durante la lettura da stdin); e scrivi a stdout.

Ecco la parte importante dell'output. L'ho annotato con alcuni commenti che indicano cosa sta succedendo nella lingua dell'assembly:

; x is in edi to begin with.  eax will be used as a temporary register.
mov  eax, edi  ; temp = x
imul eax, edi  ; temp = x * temp
imul eax, edi  ; temp = x * temp
imul eax, eax  ; temp = temp * temp

Sto usando il sistema GCC su Linux Mint 16 Petra, un derivato di Ubuntu. Ecco la versione di gcc:

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

Come hanno notato altri poster, questa opzione non è possibile in virgola mobile, poiché l'aritmetica in virgola mobile non è associativa.


12
Questo è legale per la moltiplicazione dei numeri interi perché l'overflow del complemento a due è un comportamento indefinito. Se ci sarà un overflow, accadrà da qualche parte, indipendentemente dalle operazioni di riordino. Quindi, le espressioni senza overflow valutano lo stesso, le espressioni che l'overflow sono un comportamento indefinito, quindi va bene per il compilatore cambiare il punto in cui si verifica l'overflow. gcc fa anche questo unsigned int.
Peter Cordes,

51

Perché un numero a virgola mobile a 32 bit, ad esempio 1.024, non è 1.024. In un computer, 1.024 è un intervallo: da (1.024-e) a (1.024 + e), dove "e" rappresenta un errore. Alcune persone non riescono a rendersene conto e credono anche che * in a * a rappresenti la moltiplicazione di numeri di precisione arbitraria senza che vi siano errori associati a tali numeri. Il motivo per cui alcune persone non riescono a rendersene conto sono forse i calcoli matematici che hanno esercitato nelle scuole elementari: lavorare solo con numeri ideali senza errori associati e credere che sia OK ignorare semplicemente "e" durante l'esecuzione della moltiplicazione. Non vedono la "e" implicita in "float a = 1.2", "a * a * a" e codici C simili.

Se la maggior parte dei programmatori dovesse riconoscere (ed essere in grado di eseguire) l'idea che l'espressione C a * a * a * a * a * a non stia effettivamente lavorando con numeri ideali, il compilatore GCC sarebbe quindi LIBERO per ottimizzare "a * a * a * a * a * a "in dire" t = (a * a); t * t * t "che richiede un numero minore di moltiplicazioni. Ma sfortunatamente, il compilatore GCC non sa se il programmatore che scrive il codice pensa che "a" sia un numero con o senza errore. E così GCC farà solo quello che sembra il codice sorgente - perché è quello che vede GCC a occhio nudo.

... una volta che sai che tipo di programmatore sei , puoi usare l'interruttore "-ffast-math" per dire a GCC che "Ehi, GCC, so cosa sto facendo!". Ciò consentirà a GCC di convertire un * a * a * a * a * a in un diverso testo - sembra diverso da un * a * a * a * a * a - ma calcola comunque un numero nell'intervallo di errore di a * a * a * a * a * a. Va bene, dato che sai già che stai lavorando con intervalli, non con numeri ideali.


52
I numeri in virgola mobile sono esatti. Non sono necessariamente esattamente quello che ti aspettavi. Inoltre, la tecnica con epsilon è essa stessa un'approssimazione di come affrontare le cose nella realtà, perché il vero errore atteso è relativo alla scala della mantissa, cioè, normalmente si arriva a circa 1 LSB fuori, ma che può aumentare con ogni operazione eseguita se non stai attento quindi consulta un analista numerico prima di fare qualsiasi cosa non banale con virgola mobile. Utilizzare una libreria adeguata, se possibile.
Donal Fellows,

3
@DonalFellows: lo standard IEEE richiede che i calcoli in virgola mobile producano il risultato che corrisponda più esattamente a quello che sarebbe il risultato se gli operandi di origine fossero valori esatti, ma ciò non significa che rappresentino effettivamente valori esatti. In molti casi è più utile considerare 0.1f come (1.677.722 +/- 0.5) / 16.777.216, che dovrebbe essere visualizzato con il numero di cifre decimali implicite da quell'incertezza, piuttosto che considerarla come quantità esatta (1.677.722 +/- 0,5) / 16.777.216 (che dovrebbe essere visualizzato con 24 cifre decimali).
supercat

23
@supercat: IEEE-754 è abbastanza chiaro sul punto che dati a virgola mobile fanno rappresentare i valori esatti; le clausole 3.2 - 3.4 sono le sezioni pertinenti. Ovviamente puoi scegliere di interpretarli diversamente, così come puoi scegliere di interpretarli int x = 3come significato che xè 3 +/- 0,5.
Stephen Canon,

7
@supercat: sono completamente d'accordo, ma ciò non significa che Distancenon sia esattamente uguale al suo valore numerico; significa che il valore numerico è solo un'approssimazione di una certa quantità fisica da modellare.
Stephen Canon,

10
Per l'analisi numerica, il tuo cervello ti ringrazierà se interpreti i numeri in virgola mobile non come intervalli, ma come valori esatti (che non sono esattamente i valori che volevi). Ad esempio, se x è da qualche parte intorno a 4.5 con un errore inferiore a 0,1 e si calcola (x + 1) - x, l'interpretazione "intervallo" ti lascia con un intervallo da 0,8 a 1,2, mentre l'interpretazione "valore esatto" dice il risultato sarà 1 con un errore di massimo 2 ^ (- 50) in doppia precisione.
gnasher729,

34

Nessun poster ha ancora menzionato la contrazione delle espressioni fluttuanti (standard ISO C, 6.5p8 e 7.12.2). Se il FP_CONTRACTpragma è impostato su ON, al compilatore è consentito considerare un'espressione a*a*a*a*a*acome una singola operazione, come se fosse valutata esattamente con un singolo arrotondamento. Ad esempio, un compilatore può sostituirlo con una funzione di alimentazione interna che è sia più veloce che più accurata. Ciò è particolarmente interessante in quanto il comportamento è parzialmente controllato dal programmatore direttamente nel codice sorgente, mentre le opzioni del compilatore fornite dall'utente finale possono talvolta essere utilizzate in modo errato.

Lo stato predefinito del FP_CONTRACTpragma è definito dall'implementazione, in modo che un compilatore possa eseguire tali ottimizzazioni per impostazione predefinita. Pertanto, il codice portatile che deve seguire rigorosamente le regole IEEE 754 dovrebbe impostarlo esplicitamente su OFF.

Se un compilatore non supporta questo pragma, deve essere prudente evitando tale ottimizzazione, nel caso in cui lo sviluppatore abbia scelto di impostarlo su OFF.

GCC non supporta questo pragma, ma con le opzioni predefinite, presuppone che sia ON; quindi per gli obiettivi con un FMA hardware, se si desidera impedire la trasformazione a*b+cin fma (a, b, c), è necessario fornire un'opzione come -ffp-contract=off(per impostare esplicitamente il pragma su OFF) o -std=c99(per dire a GCC di conformarsi ad alcuni Versione standard C, qui C99, quindi seguire il paragrafo precedente). In passato, quest'ultima opzione non impediva la trasformazione, il che significa che GCC non era conforme su questo punto: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845


3
Le domande popolari di lunga durata a volte mostrano la loro età. Questa domanda è stata posta e ha risposto nel 2011, quando GCC poteva essere scusato per non aver rispettato esattamente lo standard C99 allora recente. Ovviamente ora è il 2014, quindi GCC ... ahem.
Pascal Cuoq,

Tuttavia, non dovresti rispondere a domande a virgola mobile relativamente recenti senza una risposta accettata? tosse stackoverflow.com/questions/23703408 tosse
Pascal Cuoq

Lo trovo ... inquietante che gcc non implementa i pragmi in virgola mobile C99.
David Monniaux,

1
I pragmi @DavidMonniaux sono per definizione facoltativi da implementare.
Tim Seguine,

2
@TimSeguine Ma se un pragma non è implementato, il suo valore predefinito deve essere il più restrittivo per l'implementazione. Suppongo che sia quello a cui stava pensando David. Con GCC, questo è stato risolto per FP_CONTRACT se si utilizza una modalità ISO C : non implementa ancora il pragma, ma in una modalità ISO C, ora presuppone che il pragma sia disattivato.
vinc17,

28

Come ha sottolineato Lambdageek, la moltiplicazione float non è associativa e puoi ottenere meno accuratezza, ma anche quando ottieni una maggiore precisione puoi contestare l'ottimizzazione, perché vuoi un'applicazione deterministica. Ad esempio nella simulazione di gioco client / server, in cui ogni client deve simulare lo stesso mondo in cui si desidera che i calcoli in virgola mobile siano deterministici.


3
@greggo No, allora è ancora deterministico. Nessuna casualità viene aggiunta in alcun senso della parola.
Alice,

9
@Alice Sembra abbastanza chiaro che qui Bjorn sta usando 'deterministico' nel senso del codice dando lo stesso risultato su piattaforme diverse e versioni diverse del compilatore ecc. (Variabili esterne che possono essere al di fuori del controllo del programmatore) - al contrario della mancanza della casualità numerica effettiva in fase di esecuzione. Se stai sottolineando che questo non è un uso corretto della parola, non ho intenzione di discuterne.
Greggo,

5
@greggo Tranne che nella tua interpretazione di ciò che dice, è ancora sbagliato; questo è l'intero punto di IEEE 754, per fornire caratteristiche identiche per la maggior parte (se non tutte) le operazioni su piattaforme. Ora, non ha fatto menzione di piattaforme o versioni di compilatori, il che sarebbe una preoccupazione valida se si desidera che ogni singola operazione su ogni server / client remoto sia identica ... ma questo non è ovvio dalla sua affermazione. Una parola migliore potrebbe essere "in modo affidabile simile" o qualcosa del genere.
Alice,

8
@Alice stai sprecando il tempo di tutti, incluso il tuo, discutendo di semantica. Il suo significato era chiaro.
Lanaru,

11
@Lanaru L'intero punto degli standard è la semantica; il suo significato era decisamente non chiaro.
Alice,

28

Le funzioni di libreria come "pow" sono di solito realizzate con cura per produrre il minimo errore possibile (nel caso generico). Questo di solito si ottiene approssimando le funzioni con le spline (secondo il commento di Pascal l'implementazione più comune sembra usare l' algoritmo Remez )

fondamentalmente la seguente operazione:

pow(x,y);

ha un errore intrinseco approssimativamente della stessa entità dell'errore in ogni singola moltiplicazione o divisione .

Durante la seguente operazione:

float a=someValue;
float b=a*a*a*a*a*a;

ha un errore intrinseco che è maggiore di oltre 5 volte l'errore di una singola moltiplicazione o divisione (perché si stanno combinando 5 moltiplicazioni).

Il compilatore dovrebbe essere molto attento al tipo di ottimizzazione che sta facendo:

  1. se l'ottimizzazione pow(a,6)di a*a*a*a*a*aesso può migliorare le prestazioni, ma riducono drasticamente la precisione di numeri in virgola mobile.
  2. se l'ottimizzazione a*a*a*a*a*a di pow(a,6)esso può effettivamente ridurre la precisione perché "a" è stata un valore speciale che permette la moltiplicazione senza errori (una potenza di 2 o qualche piccolo numero intero)
  3. se l'ottimizzazione pow(a,6)di (a*a*a)*(a*a*a)o (a*a)*(a*a)*(a*a)c'è ancora può essere una perdita di precisione rispetto alla powfunzione.

In generale, sai che per valori in virgola mobile arbitrari "pow" ha una precisione maggiore rispetto a qualsiasi funzione che potresti eventualmente scrivere, ma in alcuni casi speciali moltiplicazioni multiple possono avere una precisione e prestazioni migliori, spetta allo sviluppatore scegliere ciò che è più appropriato, eventualmente commentando il codice in modo che nessun altro "ottimizzasse" quel codice.

L'unica cosa che ha senso (opinione personale, e apparentemente una scelta in GCC senza alcuna particolare ottimizzazione o flag del compilatore) da ottimizzare dovrebbe essere la sostituzione di "pow (a, 2)" con "a * a". Sarebbe l'unica cosa sensata che un venditore di compilatori dovrebbe fare.


7
i downvoter dovrebbero rendersi conto che questa risposta va benissimo. Posso citare dozzine di fonti e documentazioni a supporto della mia risposta e probabilmente sono più coinvolto nella precisione in virgola mobile rispetto a qualsiasi downvoter. In StackOverflow è perfettamente ragionevole aggiungere informazioni mancanti che altre risposte non coprono, quindi sii educato e spiega le tue ragioni.
CoffeDeveloper

1
Mi sembra che la risposta di Stephen Canon riguardi ciò che hai da dire. Sembra insistere sul fatto che i libm siano implementati con le spline: usano più tipicamente la riduzione degli argomenti (a seconda della funzione implementata) più un singolo polinomio i cui coefficienti sono stati ottenuti da varianti più o meno sofisticate dell'algoritmo di Remez. La scorrevolezza nei punti di giunzione non è considerata un obiettivo che vale la pena perseguire per le funzioni libm (se finiscono con una precisione sufficiente, sono comunque automaticamente abbastanza lisce, indipendentemente dal numero di parti in cui è stato diviso il dominio).
Pascal Cuoq,

La seconda metà della tua risposta manca completamente al punto che si suppone che i compilatori producano codice che implementa ciò che dice il codice sorgente, punto. Inoltre usi la parola "precisione" quando intendi "precisione".
Pascal Cuoq,

Grazie per il tuo contributo, ho leggermente corretto la risposta, qualcosa di nuovo è ancora presente nelle ultime 2 righe ^^
CoffeDeveloper

27

Non mi sarei aspettato che questo caso fosse ottimizzato. Non può essere molto spesso dove un'espressione contiene sottoespressioni che possono essere raggruppate per rimuovere intere operazioni. Mi aspetto che gli autori di compilatori investano il loro tempo in aree che avrebbero maggiori probabilità di portare a notevoli miglioramenti, piuttosto che coprire un caso limite riscontrato di rado.

Sono stato sorpreso di apprendere dalle altre risposte che questa espressione poteva davvero essere ottimizzata con gli switch del compilatore appropriati. O l'ottimizzazione è banale, oppure è un caso limite di un'ottimizzazione molto più comune, oppure gli autori del compilatore sono stati estremamente accurati.

Non c'è niente di sbagliato nel fornire suggerimenti al compilatore come hai fatto qui. È una parte normale e attesa del processo di micro-ottimizzazione per riorganizzare le dichiarazioni e le espressioni per vedere quali differenze porteranno.

Sebbene il compilatore possa essere giustificato nel considerare le due espressioni per fornire risultati incoerenti (senza gli interruttori appropriati), non è necessario che tu sia vincolato da tale restrizione. La differenza sarà incredibilmente minuscola, al punto che se la differenza conta per te, non dovresti usare l'aritmetica in virgola mobile standard in primo luogo.


17
Come notato da un altro commentatore, questo non è vero al punto di essere assurdo; la differenza potrebbe essere compresa tra la metà e il 10% del costo e, se eseguita in un ciclo ristretto, si tradurrà in molte istruzioni sprecate per ottenere quella che potrebbe essere una quantità insignificante di precisione aggiuntiva. Dire che non dovresti usare FP standard quando stai facendo un monte carlo è un po 'come dire che dovresti sempre usare un aereo per attraversare il paese; ignora molte esternalità. Infine, questa NON è un'ottimizzazione non comune; l'analisi del codice morto e la riduzione / refactor del codice sono molto comuni.
Alice,

21

Ci sono già alcune buone risposte a questa domanda, ma per completezza ho voluto sottolineare che la sezione applicabile della norma C è 5.1.2.2.3 / 15 (che è la stessa sezione 1.9 / 9 nella Standard C ++ 11). Questa sezione afferma che gli operatori possono essere raggruppati solo se sono realmente associativi o commutativi.


12

gcc attualmente può fare questa ottimizzazione, anche per numeri in virgola mobile. Per esempio,

double foo(double a) {
  return a*a*a*a*a*a;
}

diventa

foo(double):
    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm1, %xmm0
    ret

con -O -funsafe-math-optimizations. Questo riordino viola IEEE-754, tuttavia, quindi richiede la bandiera.

I numeri interi firmati, come ha sottolineato Peter Cordes in un commento, possono eseguire questa ottimizzazione senza -funsafe-math-optimizationspoiché mantengono esattamente quando non c'è overflow e se c'è overflow si ottiene un comportamento indefinito. Quindi ottieni

foo(long):
    movq    %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rax, %rax
    ret

con solo -O. Per numeri interi senza segno, è ancora più semplice poiché funzionano con potenze mod di 2 e quindi possono essere riordinati liberamente anche in caso di overflow.


1
Godbolt link con double, int e unsigned. gcc e clang entrambi ottimizzano tutti e tre allo stesso modo (con -ffast-math)
Peter Cordes,

@PeterCordes Grazie!
Charles,
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.