【问题标题】:Branch Prediction: Writing Code to Understand it; Getting Weird Results分支预测:编写代码来理解它;得到奇怪的结果
【发布时间】:2012-12-18 13:44:06
【问题描述】:

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

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

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

但是,随着浪费时间的工作量增加,阵列之间的运行时间差异也会增加很多。

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

粘贴链接:http://pastebin.com/F0JAu3uw

【问题讨论】:

  • 不应该out = rand();out = rand() % 2;
  • 哦,天哪,大卫,我犯了一个愚蠢的错误。使用新数据进行了编辑和更新——这就解释了为什么随机数组比其他数组花费的时间更少。
  • 这种波浪纹也很有趣。
  • 顺便说一句,对什么类型的 CPU 进行了基准测试?

标签: c++ branch-prediction


【解决方案1】:

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

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

另请注意,根据 CPU 的不同,分支预测可能比仅记录上次执行分支的时间要智能得多 - 例如,重复模式并不像随机数据那么糟糕。

好的,我在茶歇时进行了一个快速而肮脏的测试,它试图反映您自己的测试方法,但不会破坏缓存,如下所示:

这是你所期望的吗?

如果我以后有空的话,我还想尝试其他的东西,因为我还没有真正了解编译器在做什么......

编辑:

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

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

【讨论】:

  • +1 实际尝试一下 - 我认为传说中的“111”和“1”一样好(即总是“1”)?如果是这样,鉴于您已经检查了“始终为零”的情况(实际上,这两个图表基本相同),这似乎有趣。
  • @FrerichRaabe 实际上应该是 '0111',但是 Excel 在将文本转换为数字时截断了标签...
  • 哦,天哪,这些结果太棒了——是的,这正是我期望看到的(好吧,我希望看到所有线都有相同的斜率,不一定是分支预测器很聪明重复模式足以在所有模式上花费相同的时间)。感谢您提供翔实的答案!
【解决方案2】:

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

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

i 是像您的ifs 这样的条件。当然编译器可能会展开这个循环,但是 pipelineLen 是传递给函数的参数,所以它可能不会。

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

由于 BTB 在 Pentium 4 处理器中只有 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 可能具有更复杂的分支预测架构。

【讨论】:

  • 请记住,正如您引用的文本所指出的那样,“16 entry BTB”指的是奔腾 4。更现代的处理器具有更高级的分支预测逻辑。
  • @JasonD 当然,我不知道你有什么类型的 CPU。以此为例说明可能发生的情况。 BTB 仍然按照相同的原则运作。例如 Sandy Bridge 有一个 BTB。英特尔的文章来自 2011 年,所以我猜它仍然适用于较新的架构。
  • 是的,你是对的,如果你想单独测量分支预测,绝对值得消除循环分支等。在我的本地 CPU 上,它清理了一些结果,但是否则影响不大。在 OP 的 CPU 上,它可能会产生更深远的影响。
  • @JasonD 这就是我想要表达的。矛盾的是,要可靠地衡量系统的性能,您必须非常了解它的架构。否则副作用可能会破坏您的结果。
猜你喜欢
  • 2010-10-20
  • 1970-01-01
  • 2015-09-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多