web-dev-qa-db-ja.com

モジュロの前と代入操作の前に、 `if`ステートメントは冗長ですか?

次のコードを検討してください:

unsigned idx;
//.. some work with idx
if( idx >= idx_max )
    idx %= idx_max;

2行目のみに簡略化できます。

idx %= idx_max;

そして、同じ結果を達成します。


次のコードに何度か出会った:

unsigned x;
//... some work with x
if( x!=0 )
  x=0;

に簡略化できます

x=0;

質問:

  • ifを使用する意味はありますか?特にARM Thumb命令セット。
  • これらのifsは省略できますか?
  • コンパイラはどのような最適化を行いますか?
46
kyb

コンパイラが何をしているのかを理解したい場合は、単にいくつかのアセンブリをプルアップする必要があります。このサイトをお勧めします(質問のコードを既に入力しています): https://godbolt.org/g/FwZZOb

最初の例はより興味深いものです。

int div(unsigned int num, unsigned int num2) {
    if( num >= num2 ) return num % num2;
    return num;
}

int div2(unsigned int num, unsigned int num2) {
    return num % num2;
}

生成:

div(unsigned int, unsigned int):          # @div(unsigned int, unsigned int)
        mov     eax, edi
        cmp     eax, esi
        jb      .LBB0_2
        xor     edx, edx
        div     esi
        mov     eax, edx
.LBB0_2:
        ret

div2(unsigned int, unsigned int):         # @div2(unsigned int, unsigned int)
        xor     edx, edx
        mov     eax, edi
        div     esi
        mov     eax, edx
        ret

基本的に、コンパイラは、非常に具体的かつ論理的な理由により、ブランチを最適化してnot除外します。整数除算が比較とほぼ同じコストである場合、分岐はほとんど意味がありません。しかし、整数除算(モジュラスは通常一緒に実行されます)は実際には非常に高価です: http://www.agner.org/optimize/instruction_tables.pdf 。数値はアーキテクチャと整数サイズによって大きく異なりますが、通常は15サイクルから100サイクルに近いレイテンシです。

モジュラスを実行する前に分岐を取ることにより、実際に多くの作業を節約できます。ただし、コンパイラは、分岐のないコードをアセンブリレベルの分岐に変換しません。それは、ブランチにも欠点があるためです。とにかくモジュラスが必要になった場合、少し時間を無駄にするだけです。

idx < idx_maxが真になる相対的な頻度を知らずに、正しい最適化について合理的な判断を下す方法はありません。そのため、コンパイラ(gccとclangは同じことを行います)は、コードを比較的透過的な方法でマップすることを選択し、この選択を開発者の手に委ねます。

したがって、そのブランチは非常に合理的な選択であったかもしれません。

2番目の分岐は、比較と割り当てが同等のコストであるため、完全に無意味でなければなりません。とはいえ、リンクを見ると、コンパイラが変数への参照を持っている場合、この最適化はまだ実行されないことがわかります。値がローカル変数である場合(デモ用コードのように)、コンパイラーは分岐を最適化します。

要するに、最初のコードはおそらく妥当な最適化であり、2番目はおそらく疲れたプログラマーでしょう。

66
Nir Friedman

既に保持している値を持つ変数の書き込みは、それを読み取るよりも遅く、既に保持されている値を見つけて、書き込みをスキップする状況がいくつかあります。一部のシステムには、すべての書き込み要求をすぐにメモリに送信するプロセッサキャッシュがあります。このような設計は今日では一般的ではありませんが、完全な読み取り/書き込みキャッシュが提供できるパフォーマンスの大幅な向上をわずかなコストで実現できるため、以前は非常に一般的でした。

上記のようなコードは、一部のマルチCPU状況でも関連する可能性があります。最も一般的なこのような状況は、2つ以上のCPUコアで同時に実行されるコードが繰り返し変数にヒットする場合です。強力なメモリモデルを備えたマルチコアキャッシングシステムでは、変数を書きたいコアはまず他のコアとネゴシエートしてそれを含むキャッシュラインの排他的所有権を取得し、次に再度ネゴシエートしてそのような制御を放棄する必要があります他のコアはそれを読み書きしたい。このような操作は非常に高価になりがちであり、すべての書き込みがストレージが既に保持している値を単に保存している場合でも、費用を負担する必要があります。ただし、場所がゼロになり、再び書き込まれることがない場合、両方のコアが非排他的な読み取り専用アクセスのためにキャッシュラインを同時に保持できるため、それ以上ネゴシエートする必要はありません。

複数のCPUが変数をヒットする可能性があるほとんどすべての状況では、変数は少なくともvolatileと宣言する必要があります。ここで適用される可能性がある1つの例外は、main()の開始後に発生する変数へのすべての書き込みが同じ値を格納する場合であり、コードはストアバイの有無にかかわらず正しく動作します1つのCPUが別のCPUに表示されていました。いくつかの操作を複数回行うのは無駄ですが、そうでなければ無害であり、変数の目的がそれを行う必要があるかどうかを言うことである場合、多くの実装はvolatile修飾子なしでより良いコードを生成できるかもしれません、ただし、書き込みを無条件にして効率を改善しようとしない限りです。

ちなみに、オブジェクトがポインタを介してアクセスされた場合、上記のコードには別の理由が考えられます。特定のフィールドがゼロのconstオブジェクトまたは非constそのフィールドをゼロに設定する必要があるオブジェクト。上記のようなコードは、両方の場合に定義された動作を保証するために必要になる場合があります。

7
supercat

コードの最初のブロックに関して:これは、Clangに対するChandler Carruthの推奨に基づくマイクロ最適化です(詳細は here を参照してください)が、有効なマイクロ最適化であるとは限りませんこの形式(3進数ではなくifを使用)または任意のコンパイラで。

モジュロはかなり高価な操作です。コードが頻繁に実行され、条件の一方または他方に強い統計的リーンがある場合、CPUの分岐予測(最新のCPUを想定)は分岐命令のコストを大幅に削減します。 。

2
metamorphosis

そこにifを使用するのは悪い考えのようです。

あなたが正しいです。 idx >= idx_maxであるかどうかにかかわらず、idx %= idx_maxの後のidx_maxの下になります。 idx < idx_maxの場合、ifが続くかどうかにかかわらず、変更されません。

モジュロを介して分岐することで時間を節約できると思うかもしれませんが、実際の犯人は、分岐をたどると、現代のCPUのパイプライン処理がパイプラインをリセットする必要があり、比較的多くの時間がかかることです。整数の除算とほぼ同じ時間の整数モジュロを行うよりも、分岐に従う必要はありません。

編集:ここで他の人が示唆しているように、モジュラスはブランチに対してかなり遅いことがわかります。これとまったく同じ質問を検討している人がいます: CppCon 2015:Chandler Carruth「C++のチューニング:ベンチマーク、CPU、コンパイラ!Oh My!」 (提案)別のSO質問で、この質問への別の回答でリンクされています)。

この男はコンパイラを書いており、ブランチがなければより高速になると考えました。しかし、彼のベンチマークは彼が間違っていることを証明しました。ブランチが20%の時間しか使用されなかった場合でも、テストは高速になりました。

Ifを持たないもう1つの理由:維持するコードの行を1行少なくし、他の誰かがそれが何を意味するかを解明する。上記のリンクの男は、実際には「より速いモジュラス」マクロを作成しました。私見、この関数またはインライン関数は、ブランチがなくてもコードがはるかに理解しやすくなりますが、高速で実行されるため、パフォーマンスが重要なアプリケーションに適しています。

最後に、上のビデオの男は、この最適化をコンパイラライターに知らせることを計画しています。したがって、コードにない場合は、おそらくifが追加されます。したがって、これが発生した場合、MODのみが実行されます。

1
CodeLurker