Làm thế nào một const expr có thể được đánh giá nhanh như vậy


13

Tôi đã thử các biểu thức const được đánh giá tại thời điểm biên dịch. Nhưng tôi đã chơi với một ví dụ có vẻ cực kỳ nhanh khi được thực thi vào thời gian biên dịch.

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

Khi tôi chạy mã này, mất khoảng 7 giây để chạy. Càng xa càng tốt. Nhưng khi tôi đổi long int res = fib(45)sang const long int res = fib(45)thì không mất một giây. Theo hiểu biết của tôi, nó được đánh giá tại thời gian biên dịch. Nhưng quá trình biên dịch mất khoảng 0,3 giây

Làm thế nào trình biên dịch có thể đánh giá điều này nhanh như vậy, nhưng trong thời gian chạy thì mất nhiều thời gian hơn? Tôi đang sử dụng gcc 5.4.0.


7
Tôi phỏng đoán rằng trình biên dịch lưu trữ hàm gọi đến fib. Việc thực hiện các số Wikipedia bạn có ở trên là preeeeeetty chậm. Hãy thử lưu trữ các giá trị hàm trong mã thời gian chạy và nó sẽ nhanh hơn nhiều.
n314159

4
Trò chơi đệ quy này không hiệu quả khủng khiếp (nó có thời gian chạy theo cấp số nhân), vì vậy tôi đoán là việc đánh giá thời gian biên dịch thông minh hơn điều này và tối ưu hóa việc tính toán.
Blaze

1
@AlanBirtles Có tôi đã biên dịch nó với -O3.
Peter234

1
Tôi giả sử rằng hàm bộ đệm của trình biên dịch gọi hàm chỉ cần được bao phủ 46 lần (một lần cho mỗi đối số có thể 0-45) thay vì 2 ^ 45 lần. Tuy nhiên tôi không biết nếu gcc hoạt động như vậy.
churill

3
@Someprogrammerdude tôi biết. Nhưng làm thế nào việc biên dịch có thể nhanh chóng như vậy khi việc đánh giá mất quá nhiều thời gian khi chạy?
Peter234

Câu trả lời:


5

Trình biên dịch lưu trữ các giá trị nhỏ hơn và không cần tính toán lại nhiều như phiên bản thời gian chạy.
(Trình tối ưu hóa rất tốt và tạo ra rất nhiều mã, bao gồm cả mánh khóe với những trường hợp đặc biệt khó hiểu đối với tôi; 2 ^ 45 ngây thơ sẽ mất hàng giờ.)

Nếu bạn cũng lưu trữ các giá trị trước đó:

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

phiên bản thời gian chạy nhanh hơn nhiều so với trình biên dịch.


Không có cách nào để tránh đệ quy hai lần, trừ khi bạn thực hiện một số bộ nhớ đệm. Bạn có nghĩ rằng trình tối ưu hóa thực hiện một số bộ nhớ đệm? Bạn có thể hiển thị điều này trong đầu ra của trình biên dịch, vì điều đó sẽ thực sự thú vị?
Suma

... nó cũng là trình biên dịch có thể thay vì trình biên dịch bộ đệm có thể chứng minh một số mối quan hệ giữa sợi (n-2) và sợi (n-1) và thay vì gọi sợi (n-1), nó sử dụng để tạo sợi (n-2) ) giá trị để tính toán đó. Tôi nghĩ rằng nó phù hợp với những gì tôi thấy trong đầu ra 5,4 khi loại bỏ constexpr và sử dụng -O2.
Suma

1
Bạn có một liên kết hoặc nguồn khác giải thích những gì tối ưu hóa có thể được thực hiện tại thời gian biên dịch không?
Peter234

Miễn là hành vi quan sát được không thay đổi, trình tối ưu hóa có thể tự do làm hầu hết mọi thứ. Với fibchức năng không có tác dụng phụ (tài liệu tham khảo không có biến bên ngoài, đầu ra phụ thuộc vào chỉ đầu vào), với phần mềm tối ưu thông minh rất nhiều có thể được thực hiện.
Suma

@Suma Không có vấn đề gì khi chỉ tái diễn một lần. Vì có một phiên bản lặp, tất nhiên cũng có một phiên bản đệ quy, sử dụng ví dụ đệ quy đuôi.
Ctx

1

Bạn có thể thấy thú vị với 5,4 chức năng không bị loại bỏ hoàn toàn, bạn cần ít nhất 6,1 cho điều đó.

Tôi không nghĩ rằng có bất kỳ bộ nhớ đệm xảy ra. Tôi đã thuyết phục tôi ưu hoa là đủ thông minh để chứng minh mối quan hệ giữa fib(n - 2)fib(n-1)và tránh các cuộc gọi thứ hai hoàn toàn. Đây là đầu ra GCC 5.4 (thu được từ godbolt) không có constexprvà -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

Tôi phải thừa nhận rằng tôi không hiểu đầu ra với -O3 - mã được tạo phức tạp một cách đáng ngạc nhiên, với rất nhiều truy cập bộ nhớ và con trỏ mỹ phẩm và hoàn toàn có thể có một số bộ nhớ đệm (ghi nhớ) được thực hiện với các cài đặt đó.


Tôi nghĩ rằng tôi đã sai. Có một vòng lặp tại .L3 và sợi được lặp trên tất cả các sợi thấp hơn. Với -O2 nó vẫn theo cấp số nhân.
Suma
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.