Come può una const expr essere valutata così velocemente


13

Ho provato espressioni const che vengono valutate in fase di compilazione. Ma ho giocato con un esempio che sembra incredibilmente veloce quando eseguito in fase di compilazione.

#include<iostream> 

constexpr long int fib(int n) { 
    return (n <= 1)? n : fib(n-1) + fib(n-2); 
} 

int main () {  
    long int res = fib(45); 
    std::cout << res; 
    return 0; 
} 

Quando eseguo questo codice sono necessari circa 7 secondi per l'esecuzione. Fin qui tutto bene. Ma quando cambio long int res = fib(45)a const long int res = fib(45)non ci vuole nemmeno un secondo. Per quanto ne so, viene valutato in fase di compilazione. Ma la compilazione dura circa 0,3 secondi

In che modo il compilatore può valutarlo così rapidamente, ma in fase di esecuzione richiede molto più tempo? Sto usando gcc 5.4.0.


7
Ho ipotizzato che il compilatore memorizza nella cache la funzione a cui chiama la funzione fib. L'implementazione dei numeri di fibonacci che hai sopra è abbastanza lenta. Prova a memorizzare nella cache i valori delle funzioni nel codice di runtime e sarà molto più veloce.
n314159,

4
Questo fibonacci ricorsivo è terribilmente inefficiente (ha un tempo di esecuzione esponenziale), quindi la mia ipotesi è che la valutazione del tempo di compilazione sia più intelligente di questa e ottimizzi il calcolo.
Blaze il

1
@AlanBirtles Sì, l'ho compilato con -O3.
Peter 2334

1
Supponiamo che la funzione cache del compilatore chiama la funzione deve essere sviluppata solo 46 volte (una volta per ogni possibile argomento 0-45) anziché 2 ^ 45 volte. Comunque non so se gcc funziona così.
Churill

3
@Someprogrammerdude Lo so. Ma come può la compilazione essere così veloce quando la valutazione impiega così tanto tempo in fase di esecuzione?
Peter 2334

Risposte:


5

Il compilatore memorizza nella cache valori più piccoli e non è necessario ricalcolarlo come la versione di runtime.
(L'ottimizzatore è molto buono e genera un sacco di codice tra cui l'inganno con casi speciali che sono incomprensibili per me; le ingenue ricorsioni da 2 ^ 45 richiederebbero ore.)

Se si memorizzano anche i valori precedenti:

int cache[100] = {1, 1};

long int fib(int n) {
    int res = cache[n];
    return res ? res : (cache[n] = fib(n-1) + fib(n-2));
} 

la versione di runtime è molto più veloce del compilatore.


Non è possibile evitare di ricorrere due volte, a meno che non si esegua la memorizzazione nella cache. Pensi che l'ottimizzatore implementa un po 'di cache? Sei in grado di mostrarlo nell'output del compilatore, poiché sarebbe davvero interessante?
Suma,

... è anche possibile che il compilatore invece di memorizzare nella cache sia in grado di provare una relazione tra fib (n-2) e fib (n-1) e invece di chiamare fib (n-1) che usa per fib (n-2 ) per calcolarlo. Penso che corrisponda a quello che vedo nell'output di 5.4 quando si rimuove constexpr e si utilizza -O2.
Suma,

1
Hai un link o un'altra fonte che spiega quali ottimizzazioni possono essere fatte al momento della compilazione?
Peter 2334

Finché il comportamento osservabile rimane invariato, l'ottimizzatore è libero di fare praticamente qualsiasi cosa. La fibfunzione data non ha effetti collaterali (non fa riferimento a variabili esterne, l'output dipende solo dagli input), con un intelligente ottimizzatore si può fare molto.
Suma,

@Suma Non è un problema ricorrere una sola volta. Poiché esiste una versione iterativa, esiste ovviamente anche una versione ricorsiva, che utilizza ad esempio la ricorsione della coda.
Ctx,

1

Potresti trovare interessante con 5.4 la funzione non è completamente eliminata, devi averne almeno 6.1.

Non penso che ci sia alcuna memorizzazione nella cache. Sono convinto che l'ottimizzatore è abbastanza intelligente per dimostrare la relazione tra fib(n - 2)e fib(n-1)ed evita completamente la seconda chiamata. Questa è l'uscita GCC 5.4 (ottenuta da godbolt) con no constexpre -O2:

fib(long):
        cmp     rdi, 1
        push    r12
        mov     r12, rdi
        push    rbp
        push    rbx
        jle     .L4
        mov     rbx, rdi
        xor     ebp, ebp
.L3:
        lea     rdi, [rbx-1]
        sub     rbx, 2
        call    fib(long)
        add     rbp, rax
        cmp     rbx, 1
        jg      .L3
        and     r12d, 1
.L2:
        lea     rax, [r12+rbp]
        pop     rbx
        pop     rbp
        pop     r12
        ret
.L4:
        xor     ebp, ebp
        jmp     .L2

Devo ammettere che non capisco l'output con -O3 - il codice generato è sorprendentemente complesso, con molti accessi alla memoria e aritmetica del puntatore ed è abbastanza possibile che ci sia un po 'di memorizzazione nella cache (memoization) fatta con quelle impostazioni.


Penso di sbagliarmi. C'è un loop in .L3, e il fib è in loop su tutti i fib inferiori. Con -O2 è ancora esponenziale.
Suma,
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.