コンパイラの最適化による複雑性クラスの変更?


8

コンパイラやプロセッサの最適化戦略により、アルゴリズムの複雑さのクラスが明らかに変化している例を探しています。


2
これは、末尾の再帰を削除するなど、スペースが複雑な場合に必ず発生します。
エリオットボール

4
簡単:空ループのO(n)はO(1)を離れて最適化することができる:このSOポストを参照してくださいstackoverflow.com/questions/10300253/...
ドクブラウン

それは本当に最適ではないのですけれども、それは、Haskellでこの例を見つけることが簡単です-機能のためのコードのその潜在的に大きな塊を意味する言葉のちょうど怠惰なセマンティクス結果が使用されることはありませんので、評価されませんと呼ばれます。Haskellでは、無限リストを返す無限に再帰的な関数を定義することもよくあります。リストの有限のチャンクのみを使用している限り、有限量の再帰のみが評価され(奇妙なことに、おそらく非再帰的に)、リストのその有限部分のみが計算されます。
Steve314 2013年

1
@ Steve314:この例はコンピューター言語ベンチマークゲームにあったと思います。Haskellの実装はCの実装よりも10倍高速でした。これは、ベンチマークの結果が出力されないという単純な事実により、Haskellプログラム全体が本質的にまでコンパイルint main(void) { exit(0); };
イェルクWミッターク

@ヨルク-それは私を驚かせないでしょう、しかし私はHaskell開発者が不正行為をしていたと思います。Haskellで何かを熱心に評価する必要がある場合は、奇妙なことに主に最適化のためにそれを強制することができます-厳密な/熱心な評価は、評価を延期するためのオーバーヘッドを回避するため、レイジーよりもはるかに高速です。優れたHaskellコンパイラーは、「厳密性分析」を使用してこれ自体の多くをキャッチしますが、問題を強制しなければならない場合があります。
Steve314、2013年

回答:


4

コマンドラインに入力された数値の2乗を出力する単純なプログラムを見てみましょう。

#include <stdio.h>

int main(int argc, char **argv) {
    int num = atoi(argv[1]);
    printf("%d\n",num);
    int i = 0;
    int total = 0;
    for(i = 0; i < num; i++) {
        total += num;
    }
    printf("%d\n",total);
    return 0;
}

ご覧のとおり、これはO(n)計算であり、何度もループしています。

これをgcc -S1つでコンパイルすると、次のようなセグメントが取得されます。

LBB1_1:
        movl    -36(%rbp), %eax
        movl    -28(%rbp), %ecx
        addl    %ecx, %eax
        movl    %eax, -36(%rbp)
        movl    -32(%rbp), %eax
        addl    $1, %eax
        movl    %eax, -32(%rbp)
LBB1_2:
        movl    -32(%rbp), %eax
        movl    -28(%rbp), %ecx
        cmpl    %ecx, %eax
        jl      LBB1_1

これで、追加が行われたこと、ループの比較とジャンプバックを確認できます。

を使用してコンパイルを行いgcc -S -O3、printfの呼び出し間のセグメントを最適化します。

        callq   _printf
        testl   %ebx, %ebx
        jg      LBB1_2
        xorl    %ebx, %ebx
        jmp     LBB1_3
LBB1_2:
        imull   %ebx, %ebx
LBB1_3:
        movl    %ebx, %esi
        leaq    L_.str(%rip), %rdi
        xorb    %al, %al
        callq   _printf

代わりに、ループも追加もありません。代わりにimull、それ自体で数を乗算する呼び出しがあります。

コンパイラはループと内部の数学演算子を認識し、適切な計算に置き換えました。


これにはatoi、番号を取得するための呼び出しが含まれていることに注意してください。数値がすでにコードに存在する場合、コンパイラーは、C#とCのsqrtのパフォーマンスの比較sqrt(2)(定数)がループ全体で1,000,000回合計されたことを示すように実際の呼び出しを行うのではなく、値を事前に計算します。


7

テールコールの最適化により、スペースの複雑さが軽減される場合があります。たとえば、TCOを使用しない場合、このwhileループの再帰的な実装はワーストケースのスペースの複雑さを持ちますが、Ο(#iterations)TCOを使用すると、ワーストケースのスペースの複雑さはΟ(1)次のようになります。

// This is Scala, but it works the same way in every other language.
def loop(cond: => Boolean)(body: => Unit): Unit = if (cond) { body; loop(cond)(body) }

var i = 0
loop { i < 3 } { i += 1; println(i) }
// 1
// 2
// 3

// E.g. ECMAScript:
function loop(cond, body) {
    if (cond()) { body(); loop(cond, body); };
};

var i = 0;
loop(function { return i < 3; }, function { i++; print(i); });

これには一般的なTCOも必要ありません。必要なのは、非常に狭い特殊なケース、つまり直接テール再帰の排除だけです。

どうなるか非常にコンパイラの最適化は、単に複雑性クラスを変更しますが、実際には完全にアルゴリズムを変更していないところけれども面白いです。

Glorious Glasgow Haskell Compilerは時々これを行いますが、それは私が実際に話していることではなく、それは不正行為のようです。GHCには、ライブラリの開発者がいくつかの単純なコードパターンを検出し、それらを別のコードに置き換えることができる単純なパターンマッチング言語があります。また、Haskell標準ライブラリのGHC実装にはこれらの注釈の一部含まれているため、非効率であることがわかっている特定の関数の特定の使用法がより効率的なバージョンに書き直されます。

ただし、これらの翻訳は人間が作成したものであり、特定のケース向けに作成されたものです。

A Supercompilerは、人間の入力なしにアルゴリズムを変更することができるかもしれないが、私の知る限りなしの生産レベルのsupercompilerは、これまでに構築されています。


素晴らしい例をありがとう、そしてGHCに言及してくれてありがとう。もう1つの質問:順不同の実行はどうですか?この種の最適化によってアルゴリズムの複雑度クラスが変更された既知の例はありますか?
Lorenz Lo Sauer 2013年

0

言語が強度削減を行うビッグループを使用していることを認識しているコンパイラー(加算によってループのインデックスによる乗算を置き換える)は、その乗算の複雑さを最高でO(n log n)からO(n)に変更します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.