为什么我不是分支预测的受害者

Why am I not a victim of branch prediction?

本文关键字:受害者 分支预测 为什么      更新时间:2023-10-16

我正在编写一个函数来创建一个高斯滤波器(使用armadillo库),它可以是2D或3D,这取决于它接收的输入的维数。这是代码:

template <class ty>
ty gaussianFilter(const ty& input, double sigma)
{
    // Our filter will be initialized to the same size as our input.
    ty filter = ty(input); // Copy constructor.
    uword nRows = filter.n_rows;
    uword nCols = filter.n_cols;
    uword nSlic = filter.n_elem / (nRows*nCols); // If 2D, nSlic == 1.
    // Offsets with respect to the middle.
    double rowOffset = static_cast<double>(nRows/2);
    double colOffset = static_cast<double>(nCols/2);
    double sliceOffset = static_cast<double>(nSlic/2);
    // Counters.
    double x = 0 , y = 0, z = 0;
for (uword rowIndex = 0; rowIndex < nRows; rowIndex++) {
      x = static_cast<double>(rowIndex) - rowOffset;
      for (uword colIndex = 0; colIndex < nCols; colIndex++) {
        y = static_cast<double>(colIndex) - colOffset;
        for (uword sliIndex = 0; sliIndex < nSlic; sliIndex++) {
          z = static_cast<double>(sliIndex) - sliceOffset;
          // If-statement inside for-loop looks terribly inefficient
          // but the compiler should take care of this.
          if (nSlic == 1){ // If 2D, Gauss filter for 2D.
            filter(rowIndex*nCols + colIndex) = ...
          }
          else
          { // Gauss filter for 3D. 
            filter((rowIndex*nCols + colIndex)*nSlic + sliIndex) = ...
          }
       }    
     }
 }

正如我们所看到的,在最内部的循环中有一个if语句,它检查第三个维度(nSlic)的大小是否等于1。一旦在函数的开头进行了计算,nSlic就不会改变它的值,所以编译器应该足够聪明,可以优化条件分支,我不应该失去任何性能。

然而。。。如果我从循环中删除if语句,我将获得性能提升。

if (nSlic == 1)
  { // Gauss filter for 2D.
    for (uword rowIndex = 0; rowIndex < nRows; rowIndex++) {
      x = static_cast<double>(rowIndex) - rowOffset;
      for (uword colIndex = 0; colIndex < nCols; colIndex++) {
        y = static_cast<double>(colIndex) - colOffset;
        for (uword sliIndex = 0; sliIndex < nSlic; sliIndex++) {
          z = static_cast<double>(sliIndex) - sliceOffset;
          {filter(rowIndex*nCols + colIndex) = ...
        }
      } 
    }
  }
else
  {
    for (uword rowIndex = 0; rowIndex < nRows; rowIndex++) {
      x = static_cast<double>(rowIndex) - rowOffset;
      for (uword colIndex = 0; colIndex < nCols; colIndex++) {
        y = static_cast<double>(colIndex) - colOffset;
        for (uword sliIndex = 0; sliIndex < nSlic; sliIndex++) {
          z = static_cast<double>(sliIndex) - sliceOffset;
          {filter((rowIndex*nCols + colIndex)*nSlic + sliIndex) = ...                                     
        }
      } 
    }
  }

在用g++ -O3 -c -o main.o main.cpp编译并测量了这两个代码变体的执行时间后,我得到了以下结果:
(1000次重复,大小为2048的2D矩阵)

如果在内部:

  • 66.0453秒
  • 64.7701秒

如果在外部:

  • 64.0148秒
  • 63.6808秒

如果nSlic的值甚至没有改变,为什么编译器不优化分支?我必须重新构造代码以避免for-循环中的if-语句?

循环中有一个额外的变量会影响寄存器的使用,这可能会影响时序,即使分支预测工作正常。您需要查看生成的程序集才能知道。它还可能影响缓存命中率,而缓存命中率很难检测到。

您的错误在这里:

优化条件分支,我不应该失去任何性能

与实际执行与未知分支相关联的管道暂停相比,分支预测可能对您有很大帮助。但这仍然是一个额外的指令,仍有成本。处理器的魔力降低了无用代码的成本。。。大大减少但不是零。

编译器和HW之间的相互作用是这样的-编译器可能能够优化分支,使代码本身得到优化,但正如你所看到的,这会产生大量代码膨胀,因为它有效地复制了整个循环。某些编译器可能默认包含此优化,而其他编译器可能需要明确要求您完成此优化。

或者,如果编译器避免了这种优化,代码将保留分支,HW将尽可能地对其进行预测。这涉及到复杂的分支预测器,它们具有有限的表,因此它们所能达到的学习量有限。在本例中,您没有太多相互竞争的分支(循环、函数调用和返回,以及如果我们正在讨论的),但我们没有看到被调用函数的内部工作,它可能有更多的分支指令(清除您在外部学到的内容),或者它可能足够长,可以清除预测器可能使用的任何全局历史。很难说没有看到代码,也不知道分支预测器的确切功能(这取决于您使用的CPU版本)。

还有一点需要注意的是,它可能不一定与分支预测有关,这样更改代码可能会改变代码缓存或用于优化循环的一些内部循环缓冲区中的对齐方式(例如此),这可能会导致性能的急剧变化。唯一的方法是根据硬件计数器(perf、vtune等)运行一些分析,并测量分支数量和预测失误的变化。