if ... else ifステートメントを確率で順序付けするとどのような影響がありますか?


187

具体的には、一連のif... else ifステートメントがあり、各ステートメントがに評価される相対確率をあらかじめ何らかの方法で知っtrueている場合、それらを確率の順に並べ替えると、実行時間にどのくらいの差が生じますか?たとえば、私はこれを好むべきですか:

if (highly_likely)
  //do something
else if (somewhat_likely)
  //do something
else if (unlikely)
  //do something

これに?:

if (unlikely)
  //do something
else if (somewhat_likely)
  //do something
else if (highly_likely)
  //do something

ソートされたバージョンの方が速いことは明らかですが、読みやすさや副作用の存在のために、それらを最適でない順序で並べることができます。また、実際にコードを実行するまで、CPUが分岐予測をどの程度うまく処理できるかを判断することも困難です。

それで、これを実験している間に、特定のケースについて自分の質問に答えることになりましたが、他の意見/洞察も聞きたいです。

重要:この質問はif、プログラムの動作に他の影響を与えることなく、ステートメントを任意に並べ替えることができることを前提としています。私の回答では、3つの条件テストは相互に排他的であり、副作用はありません。確かに、望ましい動作を実現するためにステートメントを特定の順序で評価する必要がある場合、効率性の問題は疑わしいものです。


35
条件が相互に排他的であるというメモを追加することをお勧めします。それ以外の場合、2つのバージョンは同等ではありません
idclev 463035818

28
自己回答型の質問が1時間で20以上の賛成票を獲得し、かなり貧弱な回答になったのは興味深いことです。OPで何も呼び出さないが、賛成者はバンドワゴンにジャンプすることに注意する必要があります。質問は興味深いかもしれませんが、結果は疑わしいものです。
luk32

3
ある比較にヒットすると別の比較にヒットすることを拒否するので、これは短絡評価の形式として説明できると思います。個人的には、1つの高速比較(ブールなど)によって、リソースを大量に消費する文字列操作、正規表現、またはデータベースの相互作用を伴う可能性のある別の比較に入ることができない場合に、このような実装を優先します。
MonkeyZeus

11
一部のコンパイラーは、行われた分岐に関する統計を収集し、それらをコンパイラーにフィードバックして、最適化を向上させる機能を提供します。

11
このようなパフォーマンスが問題になる場合は、おそらくプロファイルガイド付き最適化を試して、手動の結果とコンパイラの結果を比較する必要があります
Justin

回答:


96

一般的な規則として、すべてではないにしても、ほとんどのIntel CPUは、最初に前方分岐が表示されたときにそれらが実行されないことを前提としています。Godboltの作品をご覧ください。

その後、ブランチはブランチ予測キャッシュに入り、過去の動作を使用して将来のブランチ予測を通知します。

したがって、タイトなループでは、順序の乱れの影響は比較的小さくなります。分岐予測子は、どの分岐のセットが最も可能性が高いかを学習します。ループ内にかなりの量の作業がある場合、小さな違いはあまり追加されません。

一般的なコードでは、ほとんどのコンパイラーはデフォルトで(別の理由がないため)、生成されたマシンコードを、コードで注文したのとほぼ同じように注文します。したがって、ステートメントが失敗した場合、ステートメントは前方分岐です。

したがって、「最初の出会い」から最良の分岐予測を得るには、可能性の低い順に分岐を並べる必要があります。

一連の条件で何度も密にループして簡単な作業を行うマイクロベンチマークは、命令数などの小さな影響によって支配され、相対的な分岐予測の問題にはほとんど影響しません。したがって、この場合、経験則は信頼できないため、プロファイルを作成する必要あります

その上で、ベクトル化と他の多くの最適化が小さなタイトループに適用されます。

したがって、一般的なコードでは、最も可能性の高いコードをifブロック内に配置します。これにより、キャッシュされていない分岐予測ミスが最も少なくなります。タイトなループでは、一般的なルールに従って開始します。詳細を知る必要がある場合は、プロファイリングするしかありません。

当然のことながら、他のテストよりもはるかに安価なテストがあれば、これですべて解決できます。


19
1つのテストはわずかに可能性が高い場合には、しかし:それはまた、テスト自体がどのように高価検討する価値がある多くの高価なテストをしていないから節約はおそらく上回るますので、より高価な、それは価値が最初に他のテストを置くこともできます分岐予測などによる節約
psmears 2017年

リンクあなたの結論をサポートしていません提供一般的なルールとして、ほとんどすべてではないのIntelのCPUは、前方の枝が、彼らはそれらを参照してください最初の時間を取られていないと仮定します。実際、これは、結果が最初に表示される比較的あいまいなArrendale CPUにのみ当てはまります。主流のIvy BridgeとHaswellの結果は、それをまったくサポートしていません。ハスウェルは目に見えない枝の「常にフォールスルーを予測する」に非常に近く見え、アイビーブリッジはまったく明確ではありません。
BeeOnRope 2017年

一般に、CPUは以前のように実際には静的予測を使用していないと理解されています。実際、現代のIntelはおそらく確率論的なTAGE予測子のようなものを使用しています。ブランチの履歴をさまざまな履歴テーブルにハッシュし、最も長い履歴と一致するものを取得します。エイリアスを回避するために「タグ」を使用しますが、タグには数ビットしかありません。すべての履歴の長さを見逃すと、おそらく分岐の方向に必ずしも依存しないいくつかのデフォルト予測が行われます(Haswellでは、明らかにそうではないと言うことができます)。
BeeOnRope 2017年

44

次のテストを作成して、2つの異なるif... else ifブロックの実行時間を計りました。1つは確率の順序で並べ替え、もう1つは逆の順序で並べ替えました。

#include <chrono>
#include <iostream>
#include <random>
#include <algorithm>
#include <iterator>
#include <functional>

using namespace std;

int main()
{
    long long sortedTime = 0;
    long long reverseTime = 0;

    for (int n = 0; n != 500; ++n)
    {
        //Generate a vector of 5000 random integers from 1 to 100
        random_device rnd_device;
        mt19937 rnd_engine(rnd_device());
        uniform_int_distribution<int> rnd_dist(1, 100);
        auto gen = std::bind(rnd_dist, rnd_engine);
        vector<int> rand_vec(5000);
        generate(begin(rand_vec), end(rand_vec), gen);

        volatile int nLow, nMid, nHigh;
        chrono::time_point<chrono::high_resolution_clock> start, end;

        //Sort the conditional statements in order of increasing likelyhood
        nLow = nMid = nHigh = 0;
        start = chrono::high_resolution_clock::now();
        for (int& i : rand_vec) {
            if (i >= 95) ++nHigh;               //Least likely branch
            else if (i < 20) ++nLow;
            else if (i >= 20 && i < 95) ++nMid; //Most likely branch
        }
        end = chrono::high_resolution_clock::now();
        reverseTime += chrono::duration_cast<chrono::nanoseconds>(end-start).count();

        //Sort the conditional statements in order of decreasing likelyhood
        nLow = nMid = nHigh = 0;
        start = chrono::high_resolution_clock::now();
        for (int& i : rand_vec) {
            if (i >= 20 && i < 95) ++nMid;  //Most likely branch
            else if (i < 20) ++nLow;
            else if (i >= 95) ++nHigh;      //Least likely branch
        }
        end = chrono::high_resolution_clock::now();
        sortedTime += chrono::duration_cast<chrono::nanoseconds>(end-start).count();

    }

    cout << "Percentage difference: " << 100 * (double(reverseTime) - double(sortedTime)) / double(sortedTime) << endl << endl;
}

/ O2でMSVC2017を使用すると、ソートされたバージョンは、ソートされていないバージョンよりも一貫して約28%高速であることが結果からわかります。luk32のコメントに従って、2つのテストの順序も入れ替えました。これにより、顕著な違いが生じます(22%と28%)。コードは、Intel Xeon E5-2697 v2上のWindows 7で実行されました。もちろん、これは非常に問題固有のものであり、決定的な答えとして解釈されるべきではありません。


9
ただし、if... else ifステートメントを変更すると、ロジックがコードを流れる方法に大きな影響を与える可能性があるため、OPは注意する必要があります。unlikelyチェックが頻繁に出てくるではないかもしれないが、かどうかを確認するビジネスニーズがあるかもしれないunlikely最初の他人のためにチェックする前の状態。
ルークTブルックス

21
30%高速ですか?つまり、実行する必要のなかったifステートメントのおよそ%だけ高速だったということですか。かなり合理的な結果のようです。
UKMonkey

5
どうやってそれをベンチマークしましたか?どのコンパイラ、CPUなど?この結果は移植可能ではないと確信しています。
luk32 '19年

12
このマイクロベンチマークの問題は、繰り返しループするときに、CPUが最も可能性の高いブランチを特定し、キャッシュすることです。小さなタイトループで分岐が検査されなかった場合、分岐予測キャッシュに分岐がない可能性があり、CPUが分岐予測キャッシュガイダンス0で間違って推測した場合、コストははるかに高くなる可能性があります。
Yakk-Adam Nevraumont 2017年

6
このベンチマークはあまり信頼できません。でコンパイルはgcc 6.3.0g++ -O2 -march=native -std=c++14ソート条件文にわずかなエッジを与えるが、時間のほとんどない、二つの実験間のパーセント差は約5%でした。数回、実際には(分散のために)遅くなりました。ifこのようにs を注文しても、心配する価値はないと私はかなり確信しています。PGOはおそらくそのようなケースを完全に処理します
Justin

30

いいえ、できません。ターゲットシステムが影響を受けていることが確かでない限り。デフォルトでは読みやすさを優先します。

私はあなたの結果を非常に疑っています。私はあなたの例を少し修正したので、実行を逆にする方が簡単です。Ideoneは一貫して、逆順の方が速いことを示していますが、それほど多くはありません。特定の実行では、これも時々反転しました。結果は決定的ではないと思います。コリルも実際の違いを報告していません。後でodroid xu4のExynos5422 CPUを確認できます。

問題は、最近のCPUには分岐予測子があることです。データと命令の両方をプリフェッチするための専用のロジックがたくさんあり、最近のx86 CPUは、これに関してはかなりスマートです。ARMやGPUなどの一部のスリムなアーキテクチャは、これに対して脆弱である可能性があります。ただし、コンパイラとターゲットシステムの両方に大きく依存しています。

ブランチの順序の最適化は非常に壊れやすく、はかないものです。これは、ほんの一部の微調整ステップとしてのみ実行してください。

コード:

#include <chrono>
#include <iostream>
#include <random>
#include <algorithm>
#include <iterator>
#include <functional>

using namespace std;

int main()
{
    //Generate a vector of random integers from 1 to 100
    random_device rnd_device;
    mt19937 rnd_engine(rnd_device());
    uniform_int_distribution<int> rnd_dist(1, 100);
    auto gen = std::bind(rnd_dist, rnd_engine);
    vector<int> rand_vec(5000);
    generate(begin(rand_vec), end(rand_vec), gen);
    volatile int nLow, nMid, nHigh;

    //Count the number of values in each of three different ranges
    //Run the test a few times
    for (int n = 0; n != 10; ++n) {

        //Run the test again, but now sort the conditional statements in reverse-order of likelyhood
        {
          nLow = nMid = nHigh = 0;
          auto start = chrono::high_resolution_clock::now();
          for (int& i : rand_vec) {
              if (i >= 95) ++nHigh;               //Least likely branch
              else if (i < 20) ++nLow;
              else if (i >= 20 && i < 95) ++nMid; //Most likely branch
          }
          auto end = chrono::high_resolution_clock::now();
          cout << "Reverse-sorted: \t" << chrono::duration_cast<chrono::nanoseconds>(end-start).count() << "ns" << endl;
        }

        {
          //Sort the conditional statements in order of likelyhood
          nLow = nMid = nHigh = 0;
          auto start = chrono::high_resolution_clock::now();
          for (int& i : rand_vec) {
              if (i >= 20 && i < 95) ++nMid;  //Most likely branch
              else if (i < 20) ++nLow;
              else if (i >= 95) ++nHigh;      //Least likely branch
          }
          auto end = chrono::high_resolution_clock::now();
          cout << "Sorted:\t\t\t" << chrono::duration_cast<chrono::nanoseconds>(end-start).count() << "ns" << endl;
        }
        cout << endl;
    }
}

コードで行ったように、ソートされたifブロックと逆ソートされたifブロックの順序を切り替えると、パフォーマンスに同じ〜30%の違いが生じます。イデオネとコリルがなぜ違いを示さないのか、私にはわかりません。
カールトン、

確かに面白い。他のシステムのデータを取得しようとしますが、それを試してみるまでに1日かかる場合があります。質問は、特にあなたの結果に照らして興味深いですが、それらは非常に壮観なので、私はそれをクロスチェックする必要がありました。
luk32

質問は、効果何ですか?答えはノーではありません
PJTraill 2017年

うん。しかし、元の質問に対する更新の通知は受け取りません。彼らは回答の定式化を廃止した。ごめんなさい。後でコンテンツを編集して、元の質問に回答したことを指摘し、元の点を証明するいくつかの結果を示します。
luk32

これは繰り返す価値があります。「デフォルトでは読みやすさを優先します。」多くの場合、可読コードを作成すると、コードを人間が解析しにくくすることにより、(絶対的な用語で)小さなパフォーマンスの向上を試してみるよりも良い結果が得られます。
AndrewBrēza19年

26

ちょうど私の5セント。ステートメントが以下に依存する必要がある場合、順序付けの効果のようです。

  1. 各ifステートメントの確率。

  2. 反復の数。ブランチ予測子が作動する可能性があります。

  3. ありそうな/そうでないコンパイラのヒント、つまりコードレイアウト。

これらの要因を調査するために、以下の関数のベンチマークを行いました。

ordered_ifs()

for (i = 0; i < data_sz * 1024; i++) {
    if (data[i] < check_point) // highly likely
        s += 3;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (data[i] == check_point) // very unlikely
        s += 1;
}

reverse_ifs()

for (i = 0; i < data_sz * 1024; i++) {
    if (data[i] == check_point) // very unlikely
        s += 1;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (data[i] < check_point) // highly likely
        s += 3;
}

ordered_ifs_with_hints()

for (i = 0; i < data_sz * 1024; i++) {
    if (likely(data[i] < check_point)) // highly likely
        s += 3;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (unlikely(data[i] == check_point)) // very unlikely
        s += 1;
}

reverse_ifs_with_hints()

for (i = 0; i < data_sz * 1024; i++) {
    if (unlikely(data[i] == check_point)) // very unlikely
        s += 1;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (likely(data[i] < check_point)) // highly likely
        s += 3;
}

データ

データ配列には、0〜100の乱数が含まれています。

const int RANGE_MAX = 100;
uint8_t data[DATA_MAX * 1024];

static void data_init(int data_sz)
{
    int i;
        srand(0);
    for (i = 0; i < data_sz * 1024; i++)
        data[i] = rand() % RANGE_MAX;
}

結果

次の結果は、Intel i5 @ 3,2 GHzおよびG ++ 6.3.0の場合です。最初の引数はcheck_point(つまり、可能性の高いifステートメントの確率%%)で、2番目の引数はdata_sz(つまり反復回数)です。

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/50/8                   25636 ns      25635 ns      27852
ordered_ifs/75/4                    4326 ns       4325 ns     162613
ordered_ifs/75/8                   18242 ns      18242 ns      37931
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs/100/8                   3381 ns       3381 ns     207612
reversed_ifs/50/4                   5342 ns       5341 ns     126800
reversed_ifs/50/8                  26050 ns      26050 ns      26894
reversed_ifs/75/4                   3616 ns       3616 ns     193130
reversed_ifs/75/8                  15697 ns      15696 ns      44618
reversed_ifs/100/4                  3738 ns       3738 ns     188087
reversed_ifs/100/8                  7476 ns       7476 ns      93752
ordered_ifs_with_hints/50/4         5551 ns       5551 ns     125160
ordered_ifs_with_hints/50/8        23191 ns      23190 ns      30028
ordered_ifs_with_hints/75/4         3165 ns       3165 ns     218492
ordered_ifs_with_hints/75/8        13785 ns      13785 ns      50574
ordered_ifs_with_hints/100/4        1575 ns       1575 ns     437687
ordered_ifs_with_hints/100/8        3130 ns       3130 ns     221205
reversed_ifs_with_hints/50/4        6573 ns       6572 ns     105629
reversed_ifs_with_hints/50/8       27351 ns      27351 ns      25568
reversed_ifs_with_hints/75/4        3537 ns       3537 ns     197470
reversed_ifs_with_hints/75/8       16130 ns      16130 ns      43279
reversed_ifs_with_hints/100/4       3737 ns       3737 ns     187583
reversed_ifs_with_hints/100/8       7446 ns       7446 ns      93782

分析

1.注文は重要です

4K回の反復と(ほぼ)100%の非常に高評価のステートメントの確率では、差は223%と非常に大きくなります。

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/100/4                   1673 ns       1673 ns     417073
reversed_ifs/100/4                  3738 ns       3738 ns     188087

4Kの反復と非常に好きなステートメントの確率が50%の場合、違いは約14%です。

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
reversed_ifs/50/4                   5342 ns       5341 ns     126800

2.反復回数は重要です

高く評価されたステートメントの(ほぼ)100%の確率での4Kと8Kの反復の違いは、約2倍です(予想どおり)。

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs/100/8                   3381 ns       3381 ns     207612

しかし、高く評価されたステートメントの50%の確率での4Kと8Kの反復の違いは、5.5回です。

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/50/8                   25636 ns      25635 ns      27852

なぜですか?分岐予測ミスのため。上記の各ケースのブランチミスは次のとおりです。

ordered_ifs/100/4    0.01% of branch-misses
ordered_ifs/100/8    0.01% of branch-misses
ordered_ifs/50/4     3.18% of branch-misses
ordered_ifs/50/8     15.22% of branch-misses

そのため、私のi5では、ブランチプレディクターは、そうではない可能性のあるブランチと大きなデータセットに対して、見事に失敗します。

3.ヒントが少し役立つ

4K反復の結果は、50%の確率ではやや悪く、100%に近い確率ではやや良くなります。

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs_with_hints/50/4         5551 ns       5551 ns     125160
ordered_ifs_with_hints/100/4        1575 ns       1575 ns     437687

しかし、8Kの反復の場合、結果は常に少し良くなります。

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/8                   25636 ns      25635 ns      27852
ordered_ifs/100/8                   3381 ns       3381 ns     207612
ordered_ifs_with_hints/50/8        23191 ns      23190 ns      30028
ordered_ifs_with_hints/100/8        3130 ns       3130 ns     221205

したがって、ヒントも役立ちますが、ほんの少しです。

全体的な結論は次のとおりです。結果は驚くかもしれないので、常にコードをベンチマークしてください。

お役に立てば幸いです。


1
i5 Nehalem?i5 Skylake?「i5」とだけ言ってもそれほど具体的ではありません。また、g++ -O2またはを使用したと思いますが、そうする必要-O3 -fno-tree-vectorizeがあります。
Peter Cordes

興味深いことに、with_hintsは、順序付けされたものと逆のものとでまだ異なります。どこかにソースにリンクしているとよいでしょう。(例:Godboltリンク、リンク短縮が回転しないようにフルリンクが望ましい)
Peter Cordes

1
分岐予測子が4Kの入力データサイズでも十分に予測できる、つまり、数千の周期を持つループ全体で分岐の結果を記憶してベンチマークを「破る」ことができるという事実は、最新の能力の証です分岐予測子。予測子は、整列などに非常に敏感な場合があるため、一部の変更について強力な結論を出すのは難しいことに注意してください。たとえば、さまざまなケースでヒントの反対の動作に気づきましたが、予測子に影響を与えたコードレイアウトをランダムに変更するヒントで説明できます。
BeeOnRope 2017年

1
@PeterCordes私の主なポイントは、変更の結果を予測できることですが、変更前と変更後のパフォーマンスをより適切に測定できます...そして、私は-O3とプロセッサで最適化されたと述べたはずですはi5-4460 @ 3.20GHz
Andriy

19

ここでの他のいくつかの回答に基づいて、唯一の本当の答えは次のようです:それは依存します。それは少なくとも以下に依存します(ただし、この重要な順序である必要はありません)。

  • 各ブランチの相対確率。 これは、質問された元の質問です。既存の回答に基づいて、確率による順序付けが役立ついくつかの条件があるようですが、常にそうであるとは限りません。相対確率がそれほど変わらない場合は、順序が異なることはほとんどありません。ただし、最初の条件が99.999%の確率で発生し、次の条件が残りの分数である場合、最も可能性の高いものを最初に配置することがタイミングの点で有益であると想定します。
  • 各ブランチの真/偽の条件を計算するコスト。 条件をテストするための時間コストがブランチごとに非常に高い場合、これはタイミングと効率に大きな影響を与える可能性があります。たとえば、計算に1時間単位かかる(ブール変数の状態のチェックなど)条件と、計算に数十、数百、数千、または数百万時間単位かかる別の条件(たとえば、ディスク上のファイル、または大規模なデータベースに対する複雑なSQLクエリの実行)。コードが毎回条件を順番にチェックすると仮定すると、より速い条件が最初にあるはずです(最初に失敗する他の条件に依存している場合を除く)。
  • コンパイラー/インタープリター 一部のコンパイラー(またはインタープリター)には、パフォーマンスに影響を与える可能性のある別の種類の最適化が含まれている場合があります(これらの一部は、コンパイル中または実行中に特定のオプションが選択された場合にのみ存在します)。したがって、まったく同じコンパイラを使用して同じシステムで2つのコンパイルとそれ以外の点で同じコードの実行をベンチマークする場合を除き、問題の分岐の順序のみが異なる場合は、コンパイラのバリエーションに余裕を持たせる必要があります。
  • オペレーティングシステム/ハードウェア luk32およびYakkで述べたように、さまざまなCPUには独自の最適化があります(オペレーティングシステムと同様)。したがって、ここでもベンチマークは変動の影響を受けやすくなっています。
  • コードブロック実行の頻度 ブランチを含むブロックがめったにアクセスされない場合(たとえば、起動時に1回だけ)、ブランチを配置する順序はほとんど問題になりません。一方、コードの重要な部分でコードがこのコードブロックにぶつかる場合、順序付けが非常に重要になります(ベンチマークによって異なります)。

確実に知る唯一の方法は、特定のケースをベンチマークすることです。できれば、コードが最終的に実行される予定のシステムと同じ(または非常に類似した)システムで実行することが望ましいです。異なるハードウェア、オペレーティングシステムなどを備えた一連のさまざまなシステムで実行することを目的としている場合は、複数のバリエーション間でベンチマークを行って、どちらが最適かを確認することをお勧めします。あるタイプのシステムではある順序で、別のタイプのシステムでは別の順序でコードをコンパイルするのも良い考えです。

私の個人的な経験則(ほとんどの場合、ベンチマークがない場合)は、以下に基づいて注文することです。

  1. 以前の条件の結果に依存する条件、
  2. 次に、条件を計算するコスト
  3. 各ブランチの相対確率。

13

これが高性能コードで解決されるのを私が通常見ている方法は、最も読みやすい順序を維持しながら、コンパイラにヒントを提供することです。Linuxカーネルの例を次に示します

if (likely(access_ok(VERIFY_READ, from, n))) {
    kasan_check_write(to, n);
    res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
    memset(to + (n - res), 0, res);

ここでは、アクセスチェックがパスし、エラーが返されないことが前提となっていますres。これらのif句のいずれかを並べ替えようとすると、コードが混乱するだけですが、likely()and unlikely()マクロは実際には、通常のケースと例外を指摘することで読みやすさを向上させます。

これらのマクロのLinux実装は、GCC固有の機能を使用します。clangとIntel Cコンパイラは同じ構文をサポートしているようですが、MSVCにはそのような機能はありません


4
これはlikely()unlikely()マクロとマクロの定義方法を説明し、対応するコンパイラ機能に関する情報を含めることができれば、さらに役立ちます。
Nate Eldredge

1
私の知る限り、これらのヒントはコードブロックのメモリレイアウトを変更するだけであり、はいまたはいいえがジャンプにつながるかどうかを変更します。これは、たとえばメモリページを読み取る必要性(またはその欠如)に対してパフォーマンス上の利点があります。しかし、これは、else-ifの長いリスト内の条件が評価される順序を並べ替えるものではありません
Hagen von Eitzen '22

@HagenvonEitzenうーん、そうですねelse if。条件が相互に排他的であることをコンパイラが認識できないほど賢くない場合、それは良い点です。
2017年

7

また、コンパイラとコンパイルするプラットフォームにも依存します。

理論的には、最も可能性の高い条件は、コントロールジャンプをできるだけ少なくする必要があります。

通常、最も可能性の高い状態が最初になります。

if (most_likely) {
     // most likely instructions
} else 

最も人気のあるasmは、条件がtrueの場合にジャンプする条件分岐に基づいています。そのCコードは、このような疑似asmに変換される可能性があります。

jump to ELSE if not(most_likely)
// most likely instructions
jump to end
ELSE:

これは、ジャンプにより、プログラムカウンターが変更されたために、CPUが実行パイプラインをキャンセルして停止するためです(実際に一般的なパイプラインをサポートするアーキテクチャーの場合)。次に、コンパイラーについてです。コンパイラーは、統計的に最も可能性が高い条件でコントロールを取得してジャンプを少なくすることに関して、高度な最適化を適用する場合としない場合があります。


2
条件分岐が条件が真のときに発生すると述べましたが、「疑似asm」の例ではその逆を示しています。また、最近のCPUには通常分岐予測があるため、条件付きジャンプ(すべてのジャンプがはるかに少ない)がパイプラインを停止させるとは言えません。実際、分岐が行われると予測されても行われないと予測された場合、パイプラインは停止します。条件を確率の降順で並べ替えようとは思いますが、コンパイラとCPUがそれをどのように構成するかは実装に大きく依存します。
Arne Vogel

1
「not(most_likely)」を入れたので、most_likelyがtrueの場合、コントロールはジャンプせずに続行します。
NoImaginationGuy 2017年

1
「最も人気のあるasmは、条件がtrueのときにジャンプする条件付きブランチに基づいています」..どのISAですか?それは確かにx86にもARMにも当てはまりません。基本的なARM CPUの地獄(および非常に古いx86のCPU、複雑なbpsの場合でも、通常はその仮定で始まり、その後適応します)分岐予測子は、前方分岐が行わ、後方分岐が常に行われると想定するため、主張の反対です本当です。
Voo

1
私が試したコンパイラのほとんどは、簡単なテストのために上記のアプローチを使用しました。注clang実際に異なるアプローチを取ったtest2し、test3次の理由ことを示しているヒューリスティックの< 0か、== 0テストがおそらく偽になることですが、作ることができているので、両方のパス上の関数の残りのクローンを作成することを決定したcondition == false経路を通って落下。これは、関数の残りの部分が短いためにのみ実現可能です。test4もう1つ操作を追加し、上記で概説したアプローチに戻ります。
BeeOnRope 2017年

1
@ArneVogel-正しく予測された分岐は、最新のCPUのパイプラインを完全に停止させませんが、分岐しない場合よりも大幅に低下することがよくあります。(1)制御フローが連続していないため、残りの命令は連続しjmpていない有用なので、フェッチ/デコードの帯域幅が浪費されます(2)現代の大きなコアがサイクルごとに1回のフェッチしか行わないため、フェッチされたブランチ/サイクルのハード制限が1に設定されます(OTOHの最新のIntelは、2つの未実行/サイクルを実行できます)(3 )分岐予測が、連続した分岐を処理するのが難しく、高速+低速予測子の場合...
BeeOnRope

6

Lik32コードを使用して、自分のマシンでテストを再実行することにしました。私のウィンドウまたはコンパイラが高解像度は1msであると考えているため、変更する必要がありました。

mingw32-g ++。exe -O3 -Wall -std = c ++ 11 -fexceptions -g

vector<int> rand_vec(10000000);

GCCは両方の元のコードで同じ変換を行いました。

3番目は常に真でなければならないため、最初の2つの条件のみがテストされることに注意してください。ここではGCCは一種のシャーロックです。

逆行する

.L233:
        mov     DWORD PTR [rsp+104], 0
        mov     DWORD PTR [rsp+100], 0
        mov     DWORD PTR [rsp+96], 0
        call    std::chrono::_V2::system_clock::now()
        mov     rbp, rax
        mov     rax, QWORD PTR [rsp+8]
        jmp     .L219
.L293:
        mov     edx, DWORD PTR [rsp+104]
        add     edx, 1
        mov     DWORD PTR [rsp+104], edx
.L217:
        add     rax, 4
        cmp     r14, rax
        je      .L292
.L219:
        mov     edx, DWORD PTR [rax]
        cmp     edx, 94
        jg      .L293 // >= 95
        cmp     edx, 19
        jg      .L218 // >= 20
        mov     edx, DWORD PTR [rsp+96]
        add     rax, 4
        add     edx, 1 // < 20 Sherlock
        mov     DWORD PTR [rsp+96], edx
        cmp     r14, rax
        jne     .L219
.L292:
        call    std::chrono::_V2::system_clock::now()

.L218: // further down
        mov     edx, DWORD PTR [rsp+100]
        add     edx, 1
        mov     DWORD PTR [rsp+100], edx
        jmp     .L217

And sorted

        mov     DWORD PTR [rsp+104], 0
        mov     DWORD PTR [rsp+100], 0
        mov     DWORD PTR [rsp+96], 0
        call    std::chrono::_V2::system_clock::now()
        mov     rbp, rax
        mov     rax, QWORD PTR [rsp+8]
        jmp     .L226
.L296:
        mov     edx, DWORD PTR [rsp+100]
        add     edx, 1
        mov     DWORD PTR [rsp+100], edx
.L224:
        add     rax, 4
        cmp     r14, rax
        je      .L295
.L226:
        mov     edx, DWORD PTR [rax]
        lea     ecx, [rdx-20]
        cmp     ecx, 74
        jbe     .L296
        cmp     edx, 19
        jle     .L297
        mov     edx, DWORD PTR [rsp+104]
        add     rax, 4
        add     edx, 1
        mov     DWORD PTR [rsp+104], edx
        cmp     r14, rax
        jne     .L226
.L295:
        call    std::chrono::_V2::system_clock::now()

.L297: // further down
        mov     edx, DWORD PTR [rsp+96]
        add     edx, 1
        mov     DWORD PTR [rsp+96], edx
        jmp     .L224

したがって、これは、最後のケースが分岐予測を必要としないことを除いて、私たちに多くを伝えません。

今、私はifの6つの組み合わせをすべて試しました。上位2つは元の逆でソートされています。高は> = 95、低は<20、中は20-94で、それぞれ10000000回の繰り返しです。

high, low, mid: 43000000ns
mid, low, high: 46000000ns
high, mid, low: 45000000ns
low, mid, high: 44000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 44000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 45000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 42000000ns
mid, low, high: 46000000ns
high, mid, low: 46000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 43000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 44000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 48000000ns
high, mid, low: 44000000ns
low, mid, high: 44000000ns
mid, high, low: 45000000ns
low, high, mid: 45000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 46000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 45000000ns
low, high, mid: 44000000ns

high, low, mid: 42000000ns
mid, low, high: 46000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 45000000ns
low, high, mid: 44000000ns

1900020, 7498968, 601012

Process returned 0 (0x0)   execution time : 2.899 s
Press any key to continue.

では、なぜ次数が高、低、中、次に速い(限界的に)のですか

なぜなら、最も予測できないのは最後であり、したがって、分岐予測子を通過することは決してないからです。

          if (i >= 95) ++nHigh;               // most predictable with 94% taken
          else if (i < 20) ++nLow; // (94-19)/94% taken ~80% taken
          else if (i >= 20 && i < 95) ++nMid; // never taken as this is the remainder of the outfalls.

そのため、分岐は予測され、取得され、残り、

6%+(0.94 *)20%の予測ミス。

「並べ替え」

          if (i >= 20 && i < 95) ++nMid;  // 75% not taken
          else if (i < 20) ++nLow;        // 19/25 76% not taken
          else if (i >= 95) ++nHigh;      //Least likely branch

分岐は予測されません。

25%+(0.75 *)24%予測ミス

18〜23%の差(測定された差は約9%)を与えますが、%を誤って予測する代わりにサイクルを計算する必要があります。

Nehalem CPUで17サイクルのペナルティを誤って予測し、各チェックが発行するのに1サイクル(4〜5命令)かかり、ループも1サイクルかかると仮定します。データの依存関係はカウンターとループ変数ですが、予測ミスが解消されれば、タイミングに影響を与えることはありません。

したがって、「逆」の場合は、タイミングを取得します(これは、コンピュータアーキテクチャで使用される式である必要があります:定量的アプローチIIRC)。

mispredict*penalty+count+loop
0.06*17+1+1+    (=3.02)
(propability)*(first check+mispredict*penalty+count+loop)
(0.19)*(1+0.20*17+1+1)+  (= 0.19*6.4=1.22)
(propability)*(first check+second check+count+loop)
(0.75)*(1+1+1+1) (=3)
= 7.24 cycles per iteration

「ソート済み」も同じ

0.25*17+1+1+ (=6.25)
(1-0.75)*(1+0.24*17+1+1)+ (=.25*7.08=1.77)
(1-0.75-0.19)*(1+1+1+1)  (= 0.06*4=0.24)
= 8.26

(8.26-7.24)/8.26 = 13.8%対〜9%の測定値(測定値に近い!?!)

したがって、OPの明白性は明白ではありません。

これらのテストでは、より複雑なコードまたはより多くのデータ依存関係を持つ他のテストは確かに異なるため、ケースを測定します。

テストの順序を変更すると結果が変わりますが、ループスタートの配置が異なることが原因である可能性があります。これは、すべての新しいIntel CPUで16バイトに配置するのが理想的ですが、この場合はそうではありません。


4

それらを好きな論理的な順序で配置します。確かに、ブランチは遅くなる可能性がありますが、コンピュータが実行している作業の大部分をブランチにすることはできません。

コードのパフォーマンスが重要な部分に取り組んでいる場合は、論理順序、プロファイルに基づく最適化、およびその他の手法を必ず使用してください。ただし、一般的なコードの場合は、スタイルの選択のほうが実際に多いと思います。


6
分岐予測の失敗は高価です。マイクロベンチマークでは、x86には分岐予測子の大きなテーブルがあるため、コストは低く抑えられています。同じ条件でのタイトなループにより、CPUは、どれが最も可能性が高いかを自分よりよく認識します。ただし、コード全体に分岐がある場合は、分岐予測キャッシュでスロットが不足する可能性があり、CPUはデフォルトのものをすべて想定します。そのデフォルトの推測が何であるかを知ることで、コードベース全体のサイクルを節約できます。
Yakk-Adam Nevraumont 2017年

@Yakk Jackの答えは、ここで唯一正しいものです。コンパイラが最適化を行える場合は、可読性を低下させるような最適化を行わないでください。コンパイラーが代行する場合、定数の折りたたみ、デッドコードの除去、ループの展開、またはその他の最適化を行わないでしょうか?コードを記述し、プロファイルに基づく最適化(これは、コーダーが推測するのが面倒なのでこの問題を解決するための設計です)を使用し、コンパイラーが最適化するかどうかを確認します。結局、パフォーマンスが重要なコードに分岐を入れたくないのです。
Christoph Diegelmann、2017年

@Christoph死んでいることがわかっているコードは含めません。一部のイテレータは最適化するのが難しく、違い(私にとって)は重要ではないことを認識しているので、i++いつ使用するかはわかりません。これは悲観化を避けることです。デフォルトの習慣として最も可能性の高いブロックを最初に置くと、可読性が著しく低下することはありません(実際には役立つ可能性があります)。その結果、ブランチ予測に適したコードが得られます(したがって、再取得できない均一な小さなパフォーマンスブーストが得られます)後でマイクロ最適化による)++ii++++i
Yakk-Adam Nevraumont 2017年

3

if-elseステートメントの相対確率がすでにわかっている場合は、パフォーマンスの目的で、1つの条件(真の条件)のみをチェックするため、ソートされた方法を使用することをお勧めします。

ソートされていない方法で、コンパイラはすべての条件を不必要にチェックし、時間がかかります。

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