2015-10-27 22 views
30

Il seguente codice compilato con clang corre quasi 60 volte più veloce di quello compilato con gcc con bandiere identiche del compilatore (sia -O2 o -O3):Perché clang produce un codice molto più veloce di gcc per questa semplice funzione che coinvolge l'esponenziazione?

#include <iostream> 
#include <math.h> 
#include <chrono> 
#include <limits> 

long double func(int num) 
{ 
    long double i=0; 
    long double k=0.7; 

    for(int t=1; t<num; t++){ 
     for(int n=1; n<16; n++){ 
     i += pow(k,n); 
     } 
    } 
    return i; 
} 


int main() 
{ 
    volatile auto num = 3000000; // avoid constant folding 

    std::chrono::time_point<std::chrono::system_clock> start, end; 
    start = std::chrono::system_clock::now(); 

    auto i = func(num); 

    end = std::chrono::system_clock::now(); 
    std::chrono::duration<double> elapsed = end-start; 
    std::cout.precision(std::numeric_limits<long double>::max_digits10); 
    std::cout << "Result " << i << std::endl; 
    std::cout << "Elapsed time is " << elapsed.count() << std::endl; 

    return 0; 
} 

ho testato questo con tre gcc versioni e due clang versioni 3.5.1/3.6.1 e qui ci sono i tempi sulla mia macchina (per gcc 5.2.1 e clang 3.6.1):

Timing -O3:

gcc: 2.41888s 
clang: 0.0396217s 

Timing -O2:

gcc: 2.41024s 
clang: 0.0395114s 

Timing -O1:

gcc: 2.41766s 
clang: 2.43113s 

Così sembra che gcc non ottimizza questa funzione a tutti, anche al di ottimizzazione maggiore livelli. L'output di assemblaggio di clang è quasi di circa 100 righe più lunghe di gcc e non penso sia necessario pubblicarlo qui, tutto quello che posso dire è che nell'output di gcc c'è una chiamata a pow che non viene visualizzata nell'assemblaggio clang , presumibilmente perché lo standard clang lo ottimizza in una serie di chiamate intrinseche.

Poiché i risultati sono identici (cioè i = 6966764.74717416727754), la domanda è:

  1. Perché può gcc non ottimizzare questa funzione quando clang lattina?
  2. Modificare il valore di k a 1.0 e gcc diventa veloce, esiste un problema aritmetico in virgola mobile che gcc non può escludere?

Ho provato a provare static_cast e ho attivato gli avvisi per verificare se ci fosse qualche problema con le conversioni implicite, ma non proprio.

Update: Per completezza ecco i risultati per -Ofast

gcc: 0.00262204s 
clang: 0.0013267s 

Il punto è che gcc non ottimizzare il codice a O2/O3.

+1

Hai stampato il linguaggio assembly, generato da entrambi i compilatori? –

+0

Clang utilizza la stessa implementazione della libreria standard di gcc qui? Uno di questi potrebbe avere un'implementazione di Pow più veloce/meno accurata o qualcosa del genere. (Solo una supposizione) –

+0

Quanto sono identici i risultati? '6.96676e + 06' non mostra abbastanza precisione per essere sicuro. Per una velocità come questa, direi che Clang è su impostazioni che permettono cose come scambiare l'ordine del ciclo, che non produce gli stessi risultati. – user2357112

risposta

32

Da questo clang godbolt session è in grado di eseguire tutti i calcoli pow in fase di compilazione.Si sa in fase di compilazione quali sono i valori di k e n sono e semplicemente costante pieghe calcolo:

.LCPI0_0: 
    .quad 4604480259023595110  # double 0.69999999999999996 
.LCPI0_1: 
    .quad 4602498675187552091  # double 0.48999999999999994 
.LCPI0_2: 
    .quad 4599850558606658239  # double 0.34299999999999992 
.LCPI0_3: 
    .quad 4597818534454788671  # double 0.24009999999999995 
.LCPI0_4: 
    .quad 4595223380205512696  # double 0.16806999999999994 
.LCPI0_5: 
    .quad 4593141924544133109  # double 0.11764899999999996 
.LCPI0_6: 
    .quad 4590598673379842654  # double 0.082354299999999963 
.LCPI0_7: 
    .quad 4588468774839143248  # double 0.057648009999999972 
.LCPI0_8: 
    .quad 4585976388698138603  # double 0.040353606999999979 
.LCPI0_9: 
    .quad 4583799016135705775  # double 0.028247524899999984 
.LCPI0_10: 
    .quad 4581356477717521223  # double 0.019773267429999988 
.LCPI0_11: 
    .quad 4579132580613789641  # double 0.01384128720099999 
.LCPI0_12: 
    .quad 4576738892963968780  # double 0.0096889010406999918 
.LCPI0_13: 
    .quad 4574469401809764420  # double 0.0067822307284899942 
.LCPI0_14: 
    .quad 4572123587912939977  # double 0.0047475615099429958 

e srotola il ciclo interno:

.LBB0_2:        # %.preheader 
    faddl .LCPI0_0(%rip) 
    faddl .LCPI0_1(%rip) 
    faddl .LCPI0_2(%rip) 
    faddl .LCPI0_3(%rip) 
    faddl .LCPI0_4(%rip) 
    faddl .LCPI0_5(%rip) 
    faddl .LCPI0_6(%rip) 
    faddl .LCPI0_7(%rip) 
    faddl .LCPI0_8(%rip) 
    faddl .LCPI0_9(%rip) 
    faddl .LCPI0_10(%rip) 
    faddl .LCPI0_11(%rip) 
    faddl .LCPI0_12(%rip) 
    faddl .LCPI0_13(%rip) 
    faddl .LCPI0_14(%rip) 

nota, che utilizza un comando incorporato funzione (gcc documents theirs here) per calcolare pow in fase di compilazione e se si utilizza -fno-builtin non esegue questa ottimizzazione.

Se si cambia k-1.0 poi gcc is able to perform la stessa ottimizzazione:

.L3: 
    fadd %st, %st(1) #, 
    addl $1, %eax #, t 
    cmpl %eax, %edi # t, num 
    fadd %st, %st(1) #, 
    fadd %st, %st(1) #, 
    fadd %st, %st(1) #, 
    fadd %st, %st(1) #, 
    fadd %st, %st(1) #, 
    fadd %st, %st(1) #, 
    fadd %st, %st(1) #, 
    fadd %st, %st(1) #, 
    fadd %st, %st(1) #, 
    fadd %st, %st(1) #, 
    fadd %st, %st(1) #, 
    fadd %st, %st(1) #, 
    fadd %st, %st(1) #, 
    fadd %st, %st(1) #, 
    jne .L3 #, 

Anche se è un caso semplice.

Se si modifica la condizione dell'anello interno su n < 4 quindi gcc seems willing to optimize quando k = 0.7. Come indicato nei commenti alla domanda, se il compilatore non crede che lo srotolamento possa essere d'aiuto allora sarà probabilmente prudente in quanto svolgerà il suo svolgimento dal momento che c'è un compromesso tra le dimensioni del codice.

Come indicato nei commenti, sto usando una versione modificata del codice OP negli esempi Godbolt ma non modifica la conclusione sottostante.

nota come indicato in una comment above se usiamo -fno-math-errno, che ferma errno vengano impostate, gcc diffusa a similar optimization.

+0

Si noti inoltre che 'main' di clang non chiama mai' func', perché si rende conto che non ha effetti collaterali. Memorizza/ricarica solo 3000000 dallo stack, cancella eax e restituisce. gcc fa lo stesso con '-fast-math', e ottimizza anche' func' di più. Godbolt non mette un'etichetta davanti al costruttore iostream che si trova dopo 'main', quindi sembra che' main' abbia più codice di quello che effettivamente fa. –

+0

perché non usa SSE2? –

+0

@PeterCordes sì sì, se aggiungiamo un 'std :: cout << i;' a 'main', allora inline la funzione in' main', non cambia la conclusione generale. –

1

Oltre alla risposta di Shafik Yaghmour, vorrei far notare che il motivo per l'utilizzo del volatile sulla variabile num sembra avere alcun effetto è che num viene letto prima func si chiama proprio. La lettura non può essere ottimizzata, ma la chiamata di funzione può ancora essere ottimizzata. Se hai dichiarato il parametro func come riferimento a volatile, ad es. long double func(volatile int& num), ciò impedirebbe al compilatore di ottimizzare l'intera chiamata a func.