web-dev-qa-db-ja.com

確率によるステートメントであれば、順序付けの効果は何ですか。

具体的には、一連の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つの条件付きテストは相互に排他的で副作用はありません。確かに、ステートメントを望ましい順序で実行するために特定の順序で評価する必要がある場合、効率性の問題は重要ではありません。

183
Carlton

一般的な規則として、すべてではないにしてもほとんどのIntel CPUは前方分岐がそれらに最初に会ったときにとられないと仮定します。 Godboltの作品 を参照してください。

その後、分岐は分岐予測キャッシュに入り、過去の振る舞いは将来の分岐予測を通知するために使用されます。

そのため、タイトなループでは、並べ替えミスの影響は比較的小さくなります。分岐予測子は、どの分岐セットが最も可能性が高いかを学習します。ループ内に自明ではない作業量がある場合は、わずかな違いでも足りません。

一般的なコードでは、ほとんどのコンパイラはデフォルトで(別の理由で欠けています)、生成されたマシンコードをおおよそあなたのコードの中で並べたのとほぼ同じ順序で並べます。したがって、if文は失敗したときに前方分岐になります。

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

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

それに加えて、ベクトル化や他の多くの最適化は小さなタイトなループに適用されます。

そのため、一般的なコードでは、最も可能性の高いコードをifブロック内に配置すると、キャッシュされていない分岐予測ミスが最小限に抑えられます。タイトなループでは、開始するための一般的なルールに従ってください、あなたがより多くを知る必要がある場合はあなたがプロファイルする以外に選択肢はほとんどありません。

当然のことながら、いくつかのテストが他のテストよりはるかに安価であれば、これはすべて問題になります。

2つの異なるif...else ifブロックの実行タイミングを計るために、次のテストを作成しました。一方は確率の順にソートされ、もう一方は逆の順序にソートされています。

#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;
}

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

45
Carlton

ターゲットシステムが影響を受けていると本当に確信しているのでなければ、できません。デフォルトでは読みやすくなっています。

あなたの結果には疑いがあります。私はあなたの例を少し修正したので、実行を逆にするほうが簡単です。 Ideone 逆順が速いことを一貫して示していますが、大したことはありません。特定のランではこれさえ時々ひっくり返った。結果は決定的ではないと思います。 colir これも本当の違いはありません。後で私の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;
    }
}
28
luk32

私の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では、分岐予測子はそれほどありそうもない分岐や大きなデータセットに対しては見事に失敗します。

ヒントが少し役立つ

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

そのため、ヒントも役に立ちますが、ほんの少しだけです。

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

それが役立つことを願っています。

23

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

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

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

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

  1. 以前の条件の結果に依存する条件
  2. 条件を計算するコスト
  3. 各分岐の相対確率.
18
Ampersat

私が高性能コードのためにこれを解決するのを私が通常見る方法は最も読みやすい順序を保ちながらコンパイラへのヒントを提供することです。これが 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()マクロとunlikely()マクロは、実際には通常のケースで、例外は何であるかを指摘することで、読みやすさに役立ちます。

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

12
jpa

また、あなたのコンパイラとあなたがコンパイルしようとしているプラ​​ットフォームにも依存します。

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

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

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:
…

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

6

私はLik32コードを使って自分のマシンでテストを再実行することにしました。私のウィンドウやコンパイラが高解像度は1msだと思っていたので、私はそれを変更しなければなりませんでした。

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

vector<int> Rand_vec(10000000);

GCCは両方のオリジナルコードで同じ変換を行いました。

最初の2つの条件だけがテストされることに注意してください。3番目の条件は常に成り立ちます。GCCはここでは一種のSherlockです。

.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は元の逆でソートされています。 highは95以上、lowは20以下、midは20-94、それぞれ100000000回の繰り返しです。

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%の測定差)を与えますが、予測ミス%ではなく周期を計算する必要があります。

17サイクルが私のNehalem CPUのペナルティを誤って予測し、各チェックが発行するのに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

"sorted"についても同じです。

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
Surt

あなたが好きな論理的な順序でそれらを入れなさい。確かに、分岐は遅くなるかもしれませんが、分岐はあなたのコンピュータがしている仕事の大部分であるべきではありません。

あなたがコードのパフォーマンス上重要な部分に取り組んでいるならば、それから確かに論理的な順序、プロファイルに基づく最適化と他のテクニックを使います、しかし一般的なコードのために、私はそれが本当によりスタイル的な選択だと思います。

3
Jack

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

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

2
aditya rawat