【问题标题】:Understanding branch prediction efficiency了解分支预测效率
【发布时间】:2019-08-19 02:11:33
【问题描述】:

我尝试测量分支预测成本,我创建了一个小程序。

它在堆栈上创建一个小缓冲区,用随机 0/1 填充。我可以用N 设置缓冲区的大小。该代码重复导致相同1<<N随机数的分支。

现在,我已经预料到,如果 1<<N 足够大(例如 >100),那么分支预测器将无效(因为它必须预测 >100 个随机数)。然而,这些是结果(在 5820k 机器上),随着 N 的增长,程序变得更慢:

N   time
=========
8   2.2
9   2.2
10  2.2
11  2.2
12  2.3
13  4.6
14  9.5
15  11.6
16  12.7
20  12.9

作为参考,如果缓冲区用零初始化(使用注释的init),时间或多或少是恒定的,对于N 8..16,它在 1.5-1.7 之间变化。

我的问题是:分支预测器能否有效预测如此大量的随机数?如果不是,那这里发生了什么?

(更多解释:代码执行2^32个分支,不管N。所以我预计,代码运行速度相同,不管N,因为根本无法预测分支. 但是似乎如果缓冲区大小小于 4096 (N

代码如下:

#include <cstdint>
#include <iostream>

volatile uint64_t init[2] = { 314159165, 27182818 };
// volatile uint64_t init[2] = { 0, 0 };
volatile uint64_t one = 1;

uint64_t next(uint64_t s[2]) {
    uint64_t s1 = s[0];
    uint64_t s0 = s[1];
    uint64_t result = s0 + s1;
    s[0] = s0;
    s1 ^= s1 << 23;
    s[1] = s1 ^ s0 ^ (s1 >> 18) ^ (s0 >> 5);
    return result;
}

int main() {
    uint64_t s[2];
    s[0] = init[0];
    s[1] = init[1];

    uint64_t sum = 0;

#if 1
    const int N = 16;

    unsigned char buffer[1<<N];
    for (int i=0; i<1<<N; i++) buffer[i] = next(s)&1;

    for (uint64_t i=0; i<uint64_t(1)<<(32-N); i++) {
        for (int j=0; j<1<<N; j++) {
            if (buffer[j]) {
                sum += one;
            }
        }
    }
#else
    for (uint64_t i=0; i<uint64_t(1)<<32; i++) {
        if (next(s)&1) {
            sum += one;
        }
    }

#endif
    std::cout<<sum<<"\n";
}

(代码也包含非缓冲版本,使用#if 0。它的运行速度与使用N=16的缓冲版本大致相同)

这是内部循环反汇编(使用 clang 编译。它为 8..16 之间的所有 N 生成相同的代码,只是循环计数不同。Clang 展开循环两次):

  401270:       80 3c 0c 00             cmp    BYTE PTR [rsp+rcx*1],0x0
  401274:       74 07                   je     40127d <main+0xad>
  401276:       48 03 35 e3 2d 00 00    add    rsi,QWORD PTR [rip+0x2de3]        # 404060 <one>
  40127d:       80 7c 0c 01 00          cmp    BYTE PTR [rsp+rcx*1+0x1],0x0
  401282:       74 07                   je     40128b <main+0xbb>
  401284:       48 03 35 d5 2d 00 00    add    rsi,QWORD PTR [rip+0x2dd5]        # 404060 <one>
  40128b:       48 83 c1 02             add    rcx,0x2
  40128f:       48 81 f9 00 00 01 00    cmp    rcx,0x10000
  401296:       75 d8                   jne    401270 <main+0xa0>

【问题讨论】:

  • 是的,这并不奇怪。 TAGE 预测技术旨在专门处理可能需要维护数千位历史的分支。
  • 我已经在 Haswell 上运行了你的代码并重现了你的结果。此外,TMA 方法表明,当 N
  • 一般;第一次执行代码时,分支预测率“不太好”,因为没有历史记录;如果没有任何变化(您可以存储上次的结果),那么执行代码两次是没有意义的,因此 CPU 具有完整分支历史的“非常高兴的情况”在实践中几乎不会发生。衡量“过分幸福案例”的基准只会提供错误信息。
  • @Brendan:是的。但是这个问题是关于预测 4096 个随机结果真的是“过分快乐的情况”吗?对我来说,这似乎不太可能(这就是为什么我没有费心去检查perf stat。如果我检查过,这个问题就不存在了)。但事实证明,情况确实如此。当前的 CPU 分支预测器非常好,它可以记住 4096 个结果。这对我来说是一个惊喜。 20 年前,分支预测器是“强/弱”*“采用/未采用”。现在它可以做得更多。
  • @Brendan:这绝不是“纯粹无关的幻想”。举一个反例:口译员。他们经常走同一条路是很常见的。以及对您的第一条评论的回应:“如果没有任何变化(您可以存储上次的结果),那么执行代码两次是没有意义的”。那是错误的。注意,这里的分支模式只是相同的。数据可以不同(但遵循相同的路径)。就像解释器运行字节码一样。但是,无论如何,这个问题是关于理解基准测试的结果,而不是关于它是否现实。

标签: performance x86 x86-64 cpu-architecture branch-prediction


【解决方案1】:

分支预测可以如此有效。正如 Peter Cordes 建议的那样,我已经用 perf stat 检查了分支未命中。结果如下:

N   time          cycles  branch-misses (%)      approx-time
===============================================================
8    2.2   9,084,889,375         34,806 ( 0.00)    2.2
9    2.2   9,212,112,830         39,725 ( 0.00)    2.2
10   2.2   9,264,903,090      2,394,253 ( 0.06)    2.2
11   2.2   9,415,103,000      8,102,360 ( 0.19)    2.2
12   2.3   9,876,827,586     27,169,271 ( 0.63)    2.3
13   4.6  19,572,398,825    486,814,972 (11.33)    4.6
14   9.5  39,813,380,461  1,473,662,853 (34.31)    9.5
15  11.6  49,079,798,916  1,915,930,302 (44.61)   11.7
16  12.7  53,216,900,532  2,113,177,105 (49.20)   12.7
20  12.9  54,317,444,104  2,149,928,923 (50.06)   12.9

Note: branch-misses (%) is calculated for 2^32 branches

如您所见,当N&lt;=12 时,分支预测器可以预测大部分分支(令人惊讶的是:分支预测器可以记住 4096 个连续随机分支的结果!)。当N&gt;12 时,branch-misses 开始增长。在N&gt;=16,它只能预测约 50% 的正确率,这意味着它与随机抛硬币一样有效。

可以通过查看时间和分支未命中 (%) 列来估算所用时间:我添加了最后一列 approx-time。我是这样计算的:2.2+(12.9-2.2)*branch-misses %/100。如您所见,approx-time 等于time(不考虑舍入误差)。所以这个效果可以用分支预测来完美解释。

最初的意图是计算一个分支未命中的周期数(在这种特殊情况下 - 对于其他情况,这个数字可能不同):

(54,317,444,104-9,084,889,375)/(2,149,928,923-34,806) = 21.039 = ~21 cycles.

【讨论】:

  • 分支错误预测惩罚不能用单个数字来表征,因为它取决于重新启动前端需要多长时间以及在错误预测跳转之前 RS 中仍有多少未完成的工作在进行中检测到错误预测的时间。 21 个周期的惩罚对我来说有点太高了,可能表明存在前端问题。此外,您的分析没有考虑内循环最后一次迭代的潜在错误预测成本。
  • @HadiBrais:感谢您的评论。是的,branch-miss 的成本取决于很多事情。我对一个近似值感兴趣。例如,它与浮点除法的成本有何关系。哪个更快:使用难以预测的分支或 fp-divison。是的,我没有考虑上一次迭代的错误预测,因为它不会对结果产生太大影响(对于 N=8 的情况小于 1%)。我已经稍微编辑了我的答案,说计算出的成本仅适用于这种特殊情况。
  • 嗯,除法的延迟也因输入操作数而异。错误预测的成本定义为与未发生错误预测的情况相比执行时间的增加。因此,如果您想在这种特殊情况下衡量错误预测的成本,更好的方法是按照定义,将执行时间与内部和外部迭代次数相同但条件 @987654331 的循环嵌套进行比较@ 总是正确的(很容易预测)...
  • ...这允许估计正确预测if (buffer[j]) 的单个内部迭代的成本。将其乘以if (buffer[j]) 的正确预测数,然后从总执行时间中减去结果。剩下的就是所有错误预测的成本总和。最后,将此数量除以分支if (buffer[j]) 被错误预测的次数。结果是错误预测if (buffer[j])的平均成本。
  • @HadiBrais:“除法的延迟也因输入操作数而异”。嗯,你这是什么意思? float vs double,还是别的什么?我已经按照你说的方式计算了成本,我得到了大约 22 个周期 (22.074)。
猜你喜欢
  • 2013-04-21
  • 1970-01-01
  • 2014-04-25
  • 2014-03-03
  • 2011-12-09
  • 2016-07-01
  • 2011-02-01
  • 2015-11-24
  • 1970-01-01
相关资源
最近更新 更多