Як можна швидко оцінити 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.