Como uma const expr pode ser avaliada tão rapidamente

13

Eu tenho experimentado expressões const que são avaliadas em tempo de compilação. Mas eu brinquei com um exemplo que parece incrivelmente rápido quando executado em tempo de compilação.

#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 executo esse código, leva cerca de 7 segundos para executar. Por enquanto, tudo bem. Mas quando eu mudar long int res = fib(45)para const long int res = fib(45)não leva nem um segundo. No meu entender, é avaliado em tempo de compilação. Mas a compilação leva cerca de 0,3 segundos

Como o compilador pode avaliar isso tão rapidamente, mas em tempo de execução, leva muito mais tempo? Estou usando o gcc 5.4.0.

Peter234
fonte
7
Suponho que o compilador armazena em cache a função que chama fib. A implementação dos números de fibonacci que você possui acima é muito lenta. Tente armazenar em cache os valores da função no código de tempo de execução e será muito mais rápido.
n314159 23/01
4
Esse fibonacci recursivo é terrivelmente ineficiente (tem um tempo de execução exponencial), portanto, meu palpite é que a avaliação do tempo de compilação é mais inteligente que isso e otimiza o cálculo.
Blaze
11
@AlanBirtles Sim, eu compilei com -O3.
Peter234 23/01
11
Supomos que a função de cache do compilador chama a função precisa ser evacuada apenas 46 vezes (uma vez para cada argumento possível de 0 a 45) em vez de 2 ^ 45 vezes. No entanto, não sei se o gcc funciona assim.
churill 23/01
3
@Someprogrammerdude I know. Mas como a compilação pode ser tão rápida quando a avaliação leva tanto tempo em tempo de execução?
Peter234 23/01

Respostas:

5

O compilador armazena em cache valores menores e não precisa recalcular tanto quanto a versão de tempo de execução.
(O otimizador é muito bom e gera muito código, incluindo truques com casos especiais que são incompreensíveis para mim; as recursões ingênuas de 2 ^ 45 levariam horas.)

Se você também armazenar valores anteriores:

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));
} 

a versão em tempo de execução é muito mais rápida que o compilador.

molbdnilo
fonte
Não há como evitar a recorrência duas vezes, a menos que você faça algum cache. Você acha que o otimizador implementa algum cache? Você é capaz de mostrar isso na saída do compilador, pois isso seria realmente interessante?
Suma
... também é possível que o compilador, em vez de armazenar em cache, seja capaz de provar alguma relação entre fib (n-2) e fib (n-1) e, em vez de chamar fib (n-1), ele usa para fib (n-2 ) para calcular isso. Eu acho que corresponde ao que eu vejo na saída da 5.4 whne removendo constexpr e usando -O2.
Suma
11
Você tem um link ou outra fonte que explica quais otimizações podem ser feitas em tempo de compilação?
Peter234 23/01
Enquanto o comportamento observável permanecer inalterado, o otimizador estará livre para fazer quase tudo. Dada a fibfunção não tem efeitos colaterais (não faz referência a variáveis ​​externas, a saída depende apenas de entradas), com um otimizador inteligente muito pode ser feito.
Suma
@ Suma Não há problema em recursar apenas uma vez. Como existe uma versão iterativa, é claro que também existe uma versão recursiva, que usa, por exemplo, recursão de cauda.
Ctx 27/01
1

Você pode achar interessante com 5.4 que a função não é completamente eliminada, você precisa de pelo menos 6.1 para isso.

Eu não acho que haja algum cache acontecendo. Estou convencido de que o otimizador é inteligente o suficiente para provar relação entre fib(n - 2)e fib(n-1)e evita a segunda chamada completamente. Esta é a saída do GCC 5.4 (obtida do godbolt) sem 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 admitir que não entendo a saída com -O3 - o código gerado é surpreendentemente complexo, com muitos acessos à memória e aritmética de ponteiros e é bem possível que exista algum cache (memorização) feito com essas configurações.

Suma
fonte
Eu acho que estou errado. Existe um loop em .L3, e a fib está em loop em todas as fibs inferiores. Com -O2 ainda é exponencial.
Suma