【问题标题】:Why am I not a victim of branch prediction?为什么我不是分支预测的受害者?
【发布时间】:2016-03-20 14:33:57
【问题描述】:

我正在编写一个函数来创建一个高斯滤波器(使用犰狳库),它可能是 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 的二维矩阵)

If-inside:

  • 66.0453 秒
  • 64.7701 秒

如果在外面:

  • 64.0148 秒
  • 63.6808 秒

如果 nSlic 的值甚至没有改变,为什么编译器不优化分支?我必须重组代码以避免for-loop 中的if-statement?

【问题讨论】:

  • 我对你的问题感到困惑。您将 if 语句移出嵌套循环并惊讶于您的代码运行得更快?您是否希望编译器将您的第一个版本的代码转换为您的第二个版本?
  • 我相信如果if-statement 总是产生相同的结果,编译器会优化它。我的假设来自sorted vs. unsorted array。我想了解为什么不是这种情况,以及何时可以期待这样的编译器优化。
  • 哦,我明白了。不过,这不是编译器的工作。处理器处理分支预测。
  • 分支预测是一种物理内置于处理器本身的机制,以最大程度地减少循环对the pipeline中指令的影响,它与编译器优化无关。
  • @dpgomez:您想到的编译器优化称为loop unswitching。如果你使用 gcc,你可能需要指定 -O3-funswitch-loops 来启用它。

标签: c++ compiler-optimization branch-prediction


【解决方案1】:

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

或者,如果编译器避免这种优化,代码会保留分支,而硬件则尽可能地预测它。这涉及复杂的分支预测器,它们具有有限的表,因此它们可以达到的学习量有限。在这个例子中,你没有太多的竞争分支(循环、函数调用和返回,以及我们正在讨论的 if),但是我们看不到被调用函数的内部工作,它可能有更多的分支指令(刷新你在外面学到的东西),或者它可能足够长来刷新预测器可能正在使用的任何全局历史。不看代码很难说,也不知道你的分支预测器到底做了什么(这取决于你使用的 CPU 版本)。

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

【讨论】:

    【解决方案2】:

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

    【讨论】:

      【解决方案3】:

      你的错误在这里:

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-01-20
        • 2019-01-15
        • 1970-01-01
        • 2011-05-01
        • 2018-06-10
        • 1970-01-01
        • 2014-05-08
        相关资源
        最近更新 更多