Comment une const expr peut-elle être évaluée si rapidement

13

J'ai essayé des expressions const qui sont évaluées au moment de la compilation. Mais j'ai joué avec un exemple qui semble incroyablement rapide lorsqu'il est exécuté au moment de la compilation.

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

Lorsque j'exécute ce code, l'exécution prend environ 7 secondes. Jusqu'ici tout va bien. Mais quand je change long int res = fib(45)de const long int res = fib(45)trajet ne dure pas même une seconde. À ma connaissance, il est évalué au moment de la compilation. Mais la compilation prend environ 0,3 seconde

Comment le compilateur peut-il évaluer cela si rapidement, mais au moment de l'exécution, cela prend beaucoup plus de temps? J'utilise gcc 5.4.0.

Peter234
la source
7
Je suppose que le compilateur met en cache les appels à la fonction fib. La mise en œuvre des nombres de fibonacci que vous avez ci-dessus est assez lente. Essayez de mettre en cache les valeurs des fonctions dans le code d'exécution et cela sera beaucoup plus rapide.
n314159
4
Ce fibonacci récursif est terriblement inefficace (il a un temps d'exécution exponentiel), donc je suppose que l'évaluation du temps de compilation est plus intelligente que cela et optimise le calcul.
Blaze
1
@AlanBirtles Oui, je l'ai compilé avec -O3.
Peter234
1
Nous supposons que la fonction de mise en cache du compilateur appelle la fonction doit être développée seulement 46 fois (une fois pour chaque argument possible 0-45) au lieu de 2 ^ 45 fois. Cependant, je ne sais pas si gcc fonctionne comme ça.
churill
3
@Someprogrammerdude je sais. Mais comment la compilation peut-elle être si rapide alors que l'évaluation prend autant de temps à l'exécution?
Peter234

Réponses:

5

Le compilateur met en cache des valeurs plus petites et n'a pas besoin de recalculer autant que la version d'exécution.
(L'optimiseur est très bon et génère beaucoup de code, y compris la ruse avec des cas spéciaux qui me sont incompréhensibles; les récursions naïves de 2 ^ 45 prendraient des heures.)

Si vous stockez également des valeurs précédentes:

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 version d'exécution est beaucoup plus rapide que le compilateur.

molbdnilo
la source
Il n'y a aucun moyen d'éviter de répéter deux fois, sauf si vous effectuez une mise en cache. Pensez-vous que l'optimiseur implémente une mise en cache? Pouvez-vous le montrer dans la sortie du compilateur, car ce serait vraiment intéressant?
Suma
... il est également possible que le compilateur au lieu de mettre en cache le compilateur soit capable de prouver une relation entre fib (n-2) et fib (n-1) et au lieu d'appeler fib (n-1) qu'il utilise pour fib (n-2 ) valeur pour calculer cela. Je pense que cela correspond à ce que je vois dans la sortie de 5.4 en supprimant constexpr et en utilisant -O2.
Suma
1
Avez-vous un lien ou une autre source qui explique quelles optimisations peuvent être faites au moment de la compilation?
Peter234
Tant que le comportement observable est inchangé, l'optimiseur est libre de faire presque n'importe quoi. La fibfonction donnée n'a pas d'effets secondaires (ne fait référence à aucune variable externe, la sortie dépend uniquement des entrées), avec un optimiseur intelligent, beaucoup peut être fait.
Suma
@Suma Ce n'est pas un problème de ne rechuter qu'une seule fois. Puisqu'il existe une version itérative, il y a bien sûr aussi une version récursive, qui utilise par exemple la récursivité de queue.
Ctx
1

Vous pouvez trouver intéressant avec 5.4 la fonction n'est pas complètement éliminée, vous avez besoin d'au moins 6.1 pour cela.

Je ne pense pas qu'il y ait de mise en cache. Je suis convaincu que l'optimiseur est suffisamment intelligent pour prouver la relation entre fib(n - 2)et fib(n-1)et évite complètement le deuxième appel. Il s'agit de la sortie GCC 5.4 (obtenue à partir de godbolt) sans constexpret -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

Je dois admettre que je ne comprends pas la sortie avec -O3 - le code généré est étonnamment complexe, avec beaucoup d'accès à la mémoire et l'arithmétique des pointeurs et il est fort possible qu'il y ait une mise en cache (mémorisation) effectuée avec ces paramètres.

Suma
la source
Je pense que je me trompe. Il y a une boucle à .L3, et le fib boucle sur tous les fib inférieurs. Avec -O2, il est toujours exponentiel.
Suma