どのようにconst exprを非常に速く評価できますか


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)それに変更するとき、それは1秒もかかりません。私の理解では、コンパイル時に評価されます。 しかし、コンパイルには約0.3秒かかります

コンパイラーはこれをどのように迅速に評価できますか?しかし、実行時はそれよりはるかに時間がかかりますか?私はgcc 5.4.0を使用しています。


7
コンパイラがへの関数呼び出しをキャッシュしていると思いますfib。上記のフィボナッチ数の実装は、かなり遅いです。ランタイムコードで関数値をキャッシュしてみてください。
n314159

4
この再帰的なフィボナッチはひどく非効率的(実行時間は指数関数的です)なので、コンパイル時間の評価はこれよりも賢く、計算を最適化すると思います。
ブレイズ

1
@AlanBirtlesはい-O3でコンパイルしました。
Peter234

1
私は、コンパイラが関数呼び出しをキャッシュする関数が2 ^ 45回ではなく46回(可能な引数ごとに1〜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));
} 

ランタイムバージョンはコンパイラよりもはるかに高速です。


キャッシングを行わない限り、2回の再帰を回避する方法はありません。オプティマイザはキャッシングを実装していると思いますか?これは非常に興味深いので、コンパイラの出力でこれを示すことができますか?
スマ

...キャッシングの代わりにコンパイラを使用することもできます。コンパイラは、fib(n-2)とfib(n-1)の関係を証明でき、fib(n-1)を呼び出す代わりに、fib(n-2)に使用します)それを計算する値。これは、5.4の出力でconstexprを削除して-O2を使用した場合に表示されるものと一致すると思います。
スマ

1
コンパイル時にどの最適化を実行できるかを説明するリンクまたは他のソースがありますか?
Peter234

監視可能な動作が変更されない限り、オプティマイザはほとんど何でも自由に実行できます。与えられたfib関数には副作用がなく(外部変数を参照せず、出力は入力のみに依存します)、賢いオプティマイザを使用して多くのことができます。
スマ

@須磨1回だけ再帰しても問題ありません。反復バージョンがあるので、もちろん、たとえば末尾再帰を使用する再帰バージョンもあります。
Ctx

1

5.4では機能が完全に削除されるわけではありませんが、そのためには少なくとも6.1が必要です。

キャッシュが発生しているとは思いません。私は、オプティマイザが関係を証明するためにスマート十分です確信していますfib(n - 2)fib(n-1)、完全に2番目の呼び出しを回避します。これは、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にループがあり、fibはすべての下位fibをループしています。-O2を使用しても、指数関数的です。
スマ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.