Bagaimana expr dievaluasi begitu cepat

13

Saya telah mencoba ekspresi const yang dievaluasi pada waktu kompilasi. Tapi saya bermain dengan contoh yang tampak sangat cepat ketika dieksekusi pada waktu kompilasi.

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

Ketika saya menjalankan kode ini, dibutuhkan sekitar 7 detik untuk menjalankannya. Sejauh ini bagus. Tapi ketika saya mengubah long int res = fib(45)untuk const long int res = fib(45)dibutuhkan bahkan satu detik. Untuk pemahaman saya itu dievaluasi pada waktu kompilasi. Tetapi kompilasi membutuhkan waktu sekitar 0,3 detik

Bagaimana kompilator dapat mengevaluasi hal ini dengan sangat cepat, tetapi pada saat runtime dibutuhkan waktu lebih lama? Saya menggunakan gcc 5.4.0.

Peter234
sumber
7
Saya menduga bahwa kompiler melakukan cache fungsi panggilan ke fib. Implementasi angka-angka fibonacci yang Anda miliki di atas sangat lambat. Coba caching nilai-nilai fungsi dalam kode runtime dan itu akan jauh lebih cepat.
n314159
4
Fibonacci rekursif ini sangat tidak efisien (memiliki runtime eksponensial), jadi dugaan saya adalah bahwa evaluasi waktu kompilasi lebih pintar dari ini dan mengoptimalkan perhitungan.
Blaze
1
@AlanBirtles Ya saya mengkompilasinya dengan -O3.
Peter234
1
Saya berasumsi bahwa fungsi kompiler cache memanggil fungsi yang perlu di-eveluated hanya 46 kali (satu kali untuk setiap argumen yang mungkin 0-45) alih-alih 2 ^ 45 kali. Namun saya tidak tahu apakah gcc bekerja seperti itu.
churill
3
@ Pemrogrammer Bung saya tahu. Tetapi bagaimana kompilasi bisa begitu cepat ketika evaluasi membutuhkan banyak waktu saat runtime?
Peter234

Jawaban:

5

Compiler melakukan cache nilai yang lebih kecil dan tidak perlu menghitung ulang sebanyak versi runtime.
(Pengoptimal sangat baik dan menghasilkan banyak kode termasuk tipu daya dengan kasus khusus yang tidak dapat dipahami oleh saya; rekursi naif 2 ^ 45 akan memakan waktu berjam-jam.)

Jika Anda juga menyimpan nilai sebelumnya:

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

versi runtime jauh lebih cepat daripada kompiler.

molbdnilo
sumber
Tidak ada cara untuk menghindari pengulangan dua kali, kecuali Anda melakukan caching. Apakah Anda pikir pengoptimal menerapkan caching? Apakah Anda dapat menunjukkan ini di output kompiler, karena itu akan sangat menarik?
Suma
... mungkin juga kompiler alih-alih caching, kompiler dapat membuktikan beberapa hubungan antara fib (n-2) dan fib (n-1) dan alih-alih memanggil fib (n-1) yang digunakannya ke fib (n-2) ) nilai untuk menghitungnya. Saya pikir cocok dengan apa yang saya lihat di output 5,4 ketika menghapus constexpr dan menggunakan -O2.
Suma
1
Apakah Anda memiliki tautan atau sumber lain yang menjelaskan optimasi apa yang dapat dilakukan pada waktu kompilasi?
Peter234
Selama perilaku yang diamati tidak berubah, pengoptimal bebas untuk melakukan hampir semua hal. fibFungsi yang diberikan tidak memiliki efek samping (referensi tidak ada variabel eksternal, output tergantung pada input saja), dengan pengoptimal yang pintar banyak yang dapat dilakukan.
Suma
@ Suma Tidak masalah untuk kambuh hanya sekali. Karena ada versi berulang, tentu saja ada juga versi rekursif, yang menggunakan misalnya rekursi ekor.
Ctx
1

Anda mungkin menemukan menarik dengan 5,4 fungsi tidak sepenuhnya dihilangkan, Anda perlu setidaknya 6,1 untuk itu.

Saya tidak berpikir ada caching yang terjadi. Saya yakin pengoptimal cukup pintar untuk membuktikan hubungan antara fib(n - 2)dan fib(n-1)dan menghindari panggilan kedua sepenuhnya. Ini adalah keluaran GCC 5.4 (diperoleh dari godbolt) tanpa constexprdan -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

Saya harus mengakui bahwa saya tidak mengerti keluaran dengan -O3 - kode yang dihasilkan sangat kompleks, dengan banyak akses memori dan pointer arithmetics dan sangat mungkin ada beberapa caching (memoisasi) yang dilakukan dengan pengaturan tersebut.

Suma
sumber
Saya pikir saya salah. Ada satu loop di .L3, dan fib tersebut berputar di atas semua fib yang lebih rendah. Dengan -O2 masih eksponensial.
Suma