スイッチがチェーン化されているのと同じように最適化されていないのはなぜですか?


39

次のsquareの実装は、連鎖ifステートメントに期待するような一連のcmp / jeステートメントを生成します。

int square(int num) {
    if (num == 0){
        return 0;
    } else if (num == 1){
        return 1;
    } else if (num == 2){
        return 4;
    } else if (num == 3){
        return 9;
    } else if (num == 4){
        return 16;
    } else if (num == 5){
        return 25;
    } else if (num == 6){
        return 36;
    } else if (num == 7){
        return 49;
    } else {
        return num * num;
    }
}

そして、以下は返されるデータテーブルを生成します:

int square_2(int num) {
    switch (num){
        case 0: return 0;
        case 1: return 1;
        case 2: return 4;
        case 3: return 9;
        case 4: return 16;
        case 5: return 25;
        case 6: return 36;
        case 7: return 49;
        default: return num * num;
    }
}

なぜgccは一番上のものを一番下のものに最適化できないのですか?

参照用の逆アセンブリ:https ://godbolt.org/z/UP_igi

編集:興味深いことに、MSVCはスイッチケースのデータテーブルの代わりにジャンプテーブルを生成します。そして驚くべきことに、clangはそれらを同じ結果に最適化します。


3
「未定義の動作」とはどういう意味ですか?観察可能な動作が同じである限り、コンパイラは必要なアセンブリ/マシンコードを生成できます
bolov

2
@ user207421 returnsを無視します。ケースにはがないbreaksため、スイッチにも特定の実行順序があります。if / elseチェーンはすべてのブランチでリターンを持ち、この場合のセマンティクスは同等です。最適化は不可能ではありませ。反例として、iccはどの関数も最適化しません。
user1810087

9
おそらく最も簡単な答えは... gccはこの構造を確認して(まだ)最適化できないだけです。
user1810087

3
@ user1810087に同意します。コンパイラの改良プロセスの現在の境界を見つけただけです。(一部のコンパイラーによって)現在最適化可能として認識されていないサブサブケース。実際、else-ifチェーンのすべてがそのように最適化できるわけではなく、SAME変数が定数値に対してテストされるサブセットのみが最適化されます。
Roberto Caboni

1
if-elseの実行順序は上から下に異なります。それでも、コードをifステートメントで置き換えてもマシンコードは改善されませんでした。一方、このスイッチには事前に定義された実行順序はなく、基本的にはgotoジャンプテーブルの美化にすぎません。そうは言っても、コンパイラーはここで観察可能な動作を推論することが許可されているので、if-elseバージョンの不十分な最適化はまったくがっかりです。
ランディン

回答:


29

生成されたコードは、switch-case通常、ジャンプテーブルを使用します。この場合、ルックアップテーブルを介した直接の戻りは、すべての場合に戻りが含まれるという事実を利用した最適化のようです。標準はその効果を保証しませんが、コンパイラが従来のスイッチケースのジャンプテーブルの代わりに一連の比較を生成するとしたら、私は驚くでしょう。

今、に来てif-else、それは正反対です。しながら、switch-case一定時間内に実行し、拘わらず分岐の数、if-else分岐の数が少ないために最適化されています。ここで、コンパイラーは基本的に、一連の比較を記述した順序で生成することを期待します。

私が使用していたもしそうならif-else、私はほとんどの呼び出しに期待するためsquare()に最適な0または1まれに他の値のために、そして実際に私のコードを使用するための私の目的を破って、遅く私が予想よりも実行することができ原因テーブルルックアップにこれを「最適化」ifではなくのswitch。議論の余地はありますが、GCCは正しいことを行っており、clangは最適化に過度に積極的です。

誰かがコメントで、clangがこの最適化を行いif-else、同様にルックアップテーブルベースのコードを生成するリンクを共有しました。clangを使用してケースの数を2つ(およびデフォルト)に減らすと、注目すべきことが起こります。ifとswitchの両方で同じコードを再度生成しますが、今回 は、ルックアップテーブルアプローチでなく、比較と移動切り替えます。これは、スイッチを好むclangでさえ、ケースの数が少ない場合に 'if'パターンがより最適であることを知っていることを意味します!

要約すると、一連の比較if-elseとジャンプテーブルswitch-caseは、コンパイラが従う傾向があり、開発者がコードを書くときに期待する傾向がある標準パターンです。ただし、特定の特殊なケースでは、一部のコンパイラーは、より適切な最適化が得られると感じた場合に、このパターンを解除することを選択する場合があります。他のコンパイラーは、明らかに最適とは言えない場合でも、とにかくパターンに固執することを選択し、開発者が何を望んでいるかを信頼することを信頼するかもしれません。どちらも有効なアプローチであり、それぞれに長所と短所があります。


2
はい、最適化は多刃の剣です:彼らが書くもの、彼らが望むもの、彼らが得るもの、そして私たちがそのために誰を呪うのか。
Deduplicator

1
「...これをテーブルルックアップに「最適化」すると、コードの実行が予想よりも遅くなります...」この理由を説明できますか?ジャンプテーブルが、2つの可能な条件付きブランチ(入力を0とに対してチェックする1)よりも遅くなるのはなぜですか?
コーディグレイ

@CodyGrayカウントサイクルのレベルに達しなかったことを告白する必要があります-ポインターを介したメモリからのロードは、比較およびジャンプよりも多くのサイクルを要するかもしれないという直感でただ通過しましたが、私は間違っている可能性があります。ただし、この場合でも、少なくとも「0」のif方が明らかに高速であることに同意してください。ここで、ifスイッチを使用する場合よりも使用する場合に0と1の両方が高速になるプラットフォームの例を次に示します。godbolt.org / z / wcJhvS(他にも複数の最適化が行われていることに注意してください)
th33lf

1
とにかく、サイクルのカウントは、現代のスーパースカラーOOOアーキテクチャでは機能しません。:-)メモリからのロードは、誤って予測されたブランチよりも遅くなることはないので、問題は、ブランチが予測される可能性がどれだけあるかです。その質問は、明示的なifステートメントによって生成されたか、コンパイラによって自動的に生成されたかに関係なく、あらゆる種類の条件付き分岐に当てはまります。私はARMの専門家ではないので、あなたの主張が真実switchよりも速いかどうかは本当にわかりませんif。これは、誤って予測されたブランチのペナルティに依存し、実際には、どの ARM に依存するかによって異なります。
コーディグレイ

0

考えられる理由の1つは、低い値のnum可能性が高い場合、たとえば常に0の場合、最初のコードに対して生成されたコードの方が速い可能性があるということです。スイッチ用に生成されたコードは、すべての値で同じ時間かかります。

この表に従って、最良のケースを比較します。テーブルの説明については、この回答を参照してください。

もしnum == 0、「もし」なら、xor、test、je(ジャンプ付き)、retがあります。待ち時間:1 + 1 +ジャンプ。ただし、xorとtestは独立しているため、実際の実行速度は1 + 1サイクルよりも速くなります。

num < 7「スイッチ」の場合、mov、cmp、ja(ジャンプなし)、mov、retがあります。レイテンシ:2 + 1 +ジャンプなし+ 2。

ジャンプしない命令は、ジャンプする命令よりも高速です。ただし、この表ではジャンプのレイテンシが定義されていないため、どちらが優れているかはわかりません。最後の方が常に優れていて、GCCがそれを最適化できないだけの可能性があります。


1
うーん、興味深い理論ですが、ifs対switchの場合:xor、test、jmp対mov、cmp jmp。最後がジャンプである3つの命令。最良の場合は同等に見えますか?
chacham15

3
「ジャンプしない結果のジャンプ命令は、ジャンプする結果になる命令よりも速い。」重要なのは分岐予測です。
geza
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.