分支预测:编写代码来理解它;获得奇怪的结果

时间:2013-01-04 05:45:38

标签: c++ branch-prediction

我试图通过测量运行具有可预测分支的循环的时间与具有随机分支的循环来充分理解分支预测。

所以我编写了一个程序,它采用不同顺序排列的0和1的大数组(即所有0,重复0-1,所有rand),并根据当前索引是0还是1来迭代数组分支,浪费时间。

我预计难以猜测的数组需要更长时间才能运行,因为分支预测器会更频繁地猜错,并且两组数组上运行之间的时间差值将保持不变,无论数量多少浪费时间的工作。

然而,随着浪费时间的工作量的增加,阵列之间的运行时间差异增加了,很多。

Yo this graph makes no sense

(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 link:http://pastebin.com/F0JAu3uw

2 个答案:

答案 0 :(得分:20)

我认为您可能正在测量缓存/内存性能,而不是分支预测。你内在的“工作”循环正在访问越来越多的内存。这可以解释线性增长,周期性行为等。

我可能是错的,因为我没有尝试复制你的结果,但如果我是你,我会在计时其他事情之前考虑内存访问。也许将一个易变变量加到另一个变量中,而不是在数组中工作。

另请注意,根据CPU的不同,分支预测可能比记录上次分支时更加智能 - 例如,重复模式不如随机数据那么糟糕。

好的,一个快速而肮脏的测试我打破了我的茶歇,试图镜像你自己的测试方法,但没有颠倒缓存,看起来像这样:

enter image description here

这更符合您的期望吗?

如果我以后可以随时待命,我还想尝试其他的东西,因为我还没有看到编译器在做什么......

修改

而且,这是我的最终测试 - 我在汇编程序中重新编码以删除循环分支,确保每个路径中的确切数量的指令等。

More branch prediction results

我还添加了一个5位重复模式的额外案例。在我老化的Xeon上打破分支预测器似乎很难。

答案 1 :(得分:2)

除了JasonD指出的内容之外,我还要注意for循环内部存在条件,这可能会影响分支预测:

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

i&lt; pipelineLen; 是一个类似于if的条件。当然编译器可以展开这个循环,但是pipelineLen是传递给函数的参数,所以可能它没有。

我不确定这是否可以解释结果的波浪模式,但是:

  

由于奔腾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执行了for循环,那么你的BTB将会被0-1-1-0 - 1-1-1-0 - 0-1-1-0 - 1-1-1-0填充,然后它将开始重叠,所以这确实可以解释你的结果的波浪模式(一些重叠将比其他重叠更有害)。

以此为例,说明可能发生的事情,而不是文字解释。您的CPU可能具有更复杂的分支预测架构。