Как можно быстро вычислить 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.


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

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


Невозможно избежать повторения дважды, если вы не выполните некоторое кэширование. Как вы думаете, оптимизатор реализует некоторое кэширование? Вы можете показать это в выводе компилятора, поскольку это было бы действительно интересно?
— Сума

... также возможно, что компилятор вместо кэширующего компилятора может доказать некоторую связь между 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 - сгенерированный код удивительно сложен, с большим количеством обращений к памяти и арифметикой указателей, и вполне возможно, что с этими настройками выполняется некоторое кеширование (запоминание).


Я думаю, что я не прав. В .L3 есть петля, и фиб зацикливается на всех нижних фибсах. С -O2 это все еще экспоненциально.
— Сума
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.