Как можно быстро вычислить const expr

13

Я пробовал константные выражения, которые оцениваются во время компиляции. Но я играл с примером, который кажется невероятно быстрым при исполнении во время компиляции.

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

Когда я запускаю этот код, требуется около 7 секунд для запуска. Все идет нормально. Но когда я перехожу long int res = fib(45)на const long int res = fib(45)это, не требуется даже секунды. Насколько я понимаю, он оценивается во время компиляции. Но компиляция занимает около 0,3 секунды

Как компилятор может оценить это так быстро, но во время выполнения это занимает намного больше времени? Я использую GCC 5.4.0.

Peter234
источник
7
Я предполагаю, что компилятор кэширует вызовы функций fib. Реализация чисел Фибоначчи, которые у вас есть выше, идет медленно. Попробуйте кэшировать значения функций в коде времени выполнения, и это будет намного быстрее.
n314159
4
Этот рекурсивный Фибоначчи ужасно неэффективен (имеет экспоненциальное время выполнения), поэтому я предполагаю, что оценка времени компиляции более умная, чем эта, и оптимизирует вычисления.
Blaze
1
@AlanBirtles Да, я скомпилировал это с -O3.
Peter234
1
Я предполагаю, что вызов функции кеширования компилятором должен быть обработан только 46 раз (один раз для каждого возможного аргумента 0-45) вместо 2 ^ 45 раз. Однако я не знаю, работает ли gcc так.
churill
3
@ Someprogrammerdude я знаю. Но как компиляция может быть такой быстрой, когда оценка занимает так много времени во время выполнения?
Peter234

Ответы:

5

Компилятор кэширует меньшие значения и не должен пересчитывать столько, сколько делает версия времени выполнения.
(Оптимизатор очень хорош и генерирует много кода, включая хитрости с особыми случаями, которые мне непонятны; наивные 2 ^ 45 рекурсии заняли бы часы.)

Если вы также сохраняете предыдущие значения:

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

версия во время выполнения намного быстрее, чем компилятор.

molbdnilo
источник
Невозможно избежать повторения дважды, если вы не выполните некоторое кэширование. Как вы думаете, оптимизатор реализует некоторое кэширование? Вы можете показать это в выводе компилятора, поскольку это было бы действительно интересно?
Сума
... также возможно, что компилятор вместо кэширующего компилятора может доказать некоторую связь между fib (n-2) и fib (n-1) и вместо вызова fib (n-1), который он использует для fib (n-2) ) значение для вычисления этого. Я думаю, что это совпадает с тем, что я вижу в выводе 5.4 при удалении constexpr и использовании -O2.
Сума
1
У вас есть ссылка или другой источник, который объясняет, какие оптимизации можно выполнить во время компиляции?
Peter234
Пока наблюдаемое поведение остается неизменным, оптимизатор может делать практически все. Данная fibфункция не имеет побочных эффектов (не ссылается на внешние переменные, вывод зависит только от входных данных), с умным оптимизатором многое можно сделать.
Сума
@Suma Нет проблем, чтобы повторить только один раз. Поскольку существует итеративная версия, конечно, существует также рекурсивная версия, которая использует, например, хвостовую рекурсию.
Ctx
1

В версии 5.4 может показаться интересным, что эта функция полностью не устранена, для этого вам нужно как минимум 6.1.

Я не думаю, что происходит кэширование. Я убежден , что оптимизатор достаточно умен , чтобы доказать связь между fib(n - 2)и fib(n-1)и полностью исключает второй вызов. Это вывод GCC 5.4 (полученный из Godbolt) без constexprи -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

Я должен признать, что не понимаю вывод с -O3 - сгенерированный код удивительно сложен, с большим количеством обращений к памяти и арифметикой указателей, и вполне возможно, что с этими настройками выполняется некоторое кеширование (запоминание).

Suma
источник
Я думаю, что я не прав. В .L3 есть петля, и фиб зацикливается на всех нижних фибсах. С -O2 это все еще экспоненциально.
Сума