Як можна швидко оцінити expr exst


13

Я випробував вирази const, які оцінюються під час компіляції. Але я грав із прикладом, який здається неймовірно швидким, коли виконується під час компіляції.

#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
Цей рекурсивний набір жахливо неефективний (він має експоненціальну тривалість виконання), тому я гадаю, що оцінка часу компіляції є більш розумною, ніж ця, і оптимізує обчислення.
Блей

1
@AlanBirtles Так, я компілював це з -O3.
Peter234

1
Я припускаю, що функція кешування компілятора викликає функцію, яку потрібно евелювати лише 46 разів (один раз для кожного можливого аргументу 0-45) замість 2 ^ 45 разів. Однак я не знаю, чи працює gcc так.
хоріл

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) без no 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.