17

私は、予測可能な分岐を含むループとランダムな分岐を含むループを実行する時間を測定することにより、分岐予測を十分に理解しようとしています。

そこで、0 と 1 の大きな配列をさまざまな順序 (つまり、すべて 0、0-1 の繰り返し、すべて rand) に配置し、現在のインデックスが 0 か 1 かによって分岐する配列を反復処理するプログラムを作成しました。・仕事の無駄。

推測しにくい配列は実行に時間がかかると予想しました。これは、分岐予測子がより頻繁に間違って推測するためであり、2 セットの配列での実行間の時間差は、時間に関係なく同じままであると予想していました。仕事を無駄にする。

ただし、時間を浪費する作業の量が増えるにつれて、アレイ間の実行時間の差が大きくなりました。

このグラフ意味不明だな

(X 軸は時間を浪費する作業の量、Y 軸は実行時間)

誰もこの行動を理解していますか?次のコードで実行しているコードを確認できます。

#include <stdlib.h>
#include <time.h>
#include <chrono>
#include <stdio.h>
#include <iostream>
#include <vector>
using namespace std;
static const int s_iArrayLen = 999999;
static const int s_iMaxPipelineLen = 60;
static const int s_iNumTrials = 10;

int doWorkAndReturnMicrosecondsElapsed(int* vals, int pipelineLen){
        int* zeroNums = new int[pipelineLen];
        int* oneNums = new int[pipelineLen];
        for(int i = 0; i < pipelineLen; ++i)
                zeroNums[i] = oneNums[i] = 0;

        chrono::time_point<chrono::system_clock> start, end;
        start = chrono::system_clock::now();
        for(int i = 0; i < s_iArrayLen; ++i){
                if(vals[i] == 0){
                        for(int i = 0; i < pipelineLen; ++i)
                                ++zeroNums[i];
                }
                else{
                        for(int i = 0; i < pipelineLen; ++i)
                                ++oneNums[i];
                }
        }
        end = chrono::system_clock::now();
        int elapsedMicroseconds = (int)chrono::duration_cast<chrono::microseconds>(end-start).count();

        //This should never fire, it just exists to guarantee the compiler doesn't compile out our zeroNums/oneNums
        for(int i = 0; i < pipelineLen - 1; ++i)
                if(zeroNums[i] != zeroNums[i+1] || oneNums[i] != oneNums[i+1])
                        return -1;
        delete[] zeroNums;
        delete[] oneNums;
        return elapsedMicroseconds;
}

struct TestMethod{
        string name;
        void (*func)(int, int&);
        int* results;

        TestMethod(string _name, void (*_func)(int, int&)) { name = _name; func = _func; results = new int[s_iMaxPipelineLen]; }
};

int main(){
        srand( (unsigned int)time(nullptr) );

        vector<TestMethod> testMethods;
        testMethods.push_back(TestMethod("all-zero", [](int index, int& out) { out = 0; } ));
        testMethods.push_back(TestMethod("repeat-0-1", [](int index, int& out) { out = index % 2; } ));
        testMethods.push_back(TestMethod("repeat-0-0-0-1", [](int index, int& out) { out = (index % 4 == 0) ? 0 : 1; } ));
        testMethods.push_back(TestMethod("rand", [](int index, int& out) { out = rand() % 2; } ));

        int* vals = new int[s_iArrayLen];

        for(int currentPipelineLen = 0; currentPipelineLen < s_iMaxPipelineLen; ++currentPipelineLen){
                for(int currentMethod = 0; currentMethod < (int)testMethods.size(); ++currentMethod){
                        int resultsSum = 0;
                        for(int trialNum = 0; trialNum < s_iNumTrials; ++trialNum){
                                //Generate a new array...
                                for(int i = 0; i < s_iArrayLen; ++i)  
                                        testMethods[currentMethod].func(i, vals[i]);

                                //And record how long it takes
                                resultsSum += doWorkAndReturnMicrosecondsElapsed(vals, currentPipelineLen);
                        }

                        testMethods[currentMethod].results[currentPipelineLen] = (resultsSum / s_iNumTrials);
                }
        }

        cout << "\t";
        for(int i = 0; i < s_iMaxPipelineLen; ++i){
                cout << i << "\t";
        }
        cout << "\n";
        for (int i = 0; i < (int)testMethods.size(); ++i){
                cout << testMethods[i].name.c_str() << "\t";
                for(int j = 0; j < s_iMaxPipelineLen; ++j){
                        cout << testMethods[i].results[j] << "\t";
                }
                cout << "\n";
        }
        int end;
        cin >> end;
        delete[] vals;
}

Pastebin リンク: http://pastebin.com/F0JAu3uw

4

2 に答える 2

20

分岐予測よりも、キャッシュ/メモリのパフォーマンスを測定している可能性があると思います。内部の「作業」ループは、増え続けるメモリのチャンクにアクセスしています。これは、直線的な成長、周期的な行動などを説明するかもしれません.

私はあなたの結果を複製しようとしなかったので、間違っている可能性がありますが、私があなただったら、他のことを計る前にメモリアクセスを除外します. おそらく、配列で作業するのではなく、1 つの volatile 変数を別の変数に合計します。

また、CPU によっては、分岐予測は、前回分岐が行われた時刻を記録するよりもはるかにスマートになる可能性があることにも注意してください。たとえば、繰り返しパターンは、ランダム データほど悪くはありません。

わかりました、私がティーブレイクで思いついた簡単で汚いテストは、独自のテストメソッドをミラーリングしようとしましたが、キャッシュをスラッシングすることなく、次のようになります。

ここに画像の説明を入力

それはあなたが期待した以上のものですか?

コンパイラが何をしているかを実際に見ていないので、後で試してみたいことがあります...

編集:

そして、これが私の最終テストです。アセンブラーで再コーディングして、ループ分岐を削除し、各パスの正確な数の命令を確保するなどしました。

より多くの分岐予測結果

また、5 ビットの繰り返しパターンのケースを追加しました。老朽化した Xeon で分岐予測子を混乱させるのはかなり難しいようです。

于 2013-01-04T08:08:44.517 に答える
2

JasonD が指摘したことに加えて、for分岐予測に影響を与える可能性のある条件がループ内にあることにも注意したいと思います。

if(vals[i] == 0)
{
    for(int i = 0; i < pipelineLen; ++i)
        ++zeroNums[i];
}

私は < パイプラインレン; あなたifのような状態です。もちろん、コンパイラはこのループをアンロールできますが、pipelineLen は関数に渡される引数であるため、おそらくそうではありません。

これが結果の波状パターンを説明できるかどうかはわかりませんが、

Pentium 4 プロセッサでは BTB の長さは 16 エントリしかないため、16 反復よりも長いループの予測は最終的に失敗します。この制限は、ループが 16 回の繰り返しになるまで展開することで回避できます。これが完了すると、ループ条件は常に BTB に収まり、ループの出口で分岐の予測ミスが発生しなくなります。以下は、ループ展開の例です。

記事全文を読む: http://software.intel.com/en-us/articles/branch-and-loop-reorganization-to-prevent-mispredicts

したがって、ループはメモリ スループットを測定するだけでなく、BTB にも影響を与えます。

0-1リストにパターンを渡した後pipelineLen = 2、BTB で for ループを実行すると、次のようなものが満たされ、0-1-1-0 - 1-1-1-0 - 0-1-1-0 - 1-1-1-0オーバーラップが開始されるため、結果の波状パターンを実際に説明できます (一部のオーバーラップは他のものよりも有害です) )。

文字通りの説明ではなく、起こりうることの例としてこれを取り上げてください。お使いの CPU には、はるかに洗練された分岐予測アーキテクチャが搭載されている場合があります。

于 2013-01-04T15:43:43.013 に答える