【问题标题】:Avoiding conditionals and function invocations inside a loop避免循环内的条件和函数调用
【发布时间】:2013-07-12 18:37:17
【问题描述】:

我的代码如下所示:

void function(int parameter)
{
  for( ... ) // a big loop
  {
    double a = ...;
    for( ... ) // a big loop
    {
      double b = ...;

      double value;
      if(parameter == 1)
        value = some_math_expression_1(a, b);
      else if(parameter == 2)
        value = some_math_expression_2(a, b);
      ...
    }
  }
}

我的想法是根据参数我想对ab 应用一些数学表达式。这个函数执行了很多次并且必须很快,我想知道每次迭代中的那些条件分支是否会引入我可以节省的开销。

现在,我写了这样的代码:

void function(int parameter)
{
  if(parameter == 1)
    function1();
  else if(parameter == 2)
    function2();
  else
    ...
}

如果我在每个functionX() 中重复代码,我可以直接应用数学表达式。明显的问题是,当我想更改一段代码时,我必须多次执行(我现在有大约 10 个数学表达式)。

我可以使用什么方法来避免function 中的任何开销?

如果我将指向函数some_math_expression_X 的指针传递给function 会怎样(我会更改函数调用的条件)?

如果我将整个函数编码为宏 (uf) 并将数学表达式设置为参数会怎样?

如果我使用模板并将数学表达式作为指向内联函数的指针传递会怎样(这甚至可能)?

编辑:感谢您的回答。我知道我可以使用您提出的方法(指向/函数数组的指针,或依赖分支预测器)。但是,您是否对在避免开销方面更好的方法有所了解?数学表达式非常简单(类似于a*b),除了很长的循环之外,function 也被多次调用(分支预测在调用之间“存活”吗?)。

【问题讨论】:

    标签: c++


    【解决方案1】:

    您可以将函数转换为模板:

    void functionT<int PARAMETER>()
    {
      for( ... ) // a big loop
      {
        double a = ...;
        for( ... ) // a big loop
        {
          double b = ...;
    
          double value;
          if(PARAMETER == 1) //Constant condition!!!
            value = some_math_expression_1(a, b);
          else if(PARAMETER == 2)  //Constant condition!!!
            value = some_math_expression_2(a, b);
          ...
        }
      }
    }
    

    由于条件始终为真或始终为假,编译器将优化条件树并仅保留真正的数学表达式。没有分支,也没有函数调用!

    现在,您只能将其与常量参数一起使用:

    functionT<1>();
    

    但不带变量:

    int x = 1;
    functionT<x>(); //Error
    

    如果你需要,你可以做一个包装器:

    void function(int parameter)
    {
        switch (parameter)
        {
            case 1: functionT<1>(); break;
            case 2: functionT<2>(); break;
        }
    }
    

    【讨论】:

    【解决方案2】:

    别担心。现代 CPU 具有分支预测器,它们会正确预测所采用的分支。

    【讨论】:

    • 当他们尝试这样做时,这显然会产生巨大的影响,但经常失败:)
    • Modern CPU's have branch predictors 是真的。 and they will correctly predicty 几乎肯定不是真的,尤其是对于随机的 50:50 概率分支。
    • @Salgar:但这里完全不是这样。 parameter 完全可以预测该分支,这在整个嵌套循环中都不会改变。如果它(仅)一千次迭代,你就已经达到了 99% 以上的正确预测。
    • 真的很抱歉,我在看function(),它把循环移了出来。
    • @ChronoTrigger,一般来说没有。分支预测器仅在指令级起作用。如果特定分支指令的历史记录中有条目,则将使用该条目。如果没有条目,预测器将根据某些启发式进行猜测:不采用前向条件分支(进入 IF/FOR 语句),采用后向条件分支(总是循环 WHILE 语句),采用无条件分支(总是调用函数,遵循 goto 语句)。
    【解决方案3】:

    您可以设置一个常量函数指针数组,并调用与parameter关联的函数。

    但如果数学表达式相当小,switch() 语句可能会更快。

    switch (parameter) {
        case 1:
            value = math expression 1;
            break;
        case 2:
            ...
    }
    

    【讨论】:

      【解决方案4】:

      首先,我会一如既往地说,您现在应该对这个过程需要多长时间进行基准测试/测量,因为与往常一样,这可能是过早的优化,您可能会发现这不是您的代码的一部分需要很长时间。

      但假设您已经测量并发现这是您代码中的瓶颈,我会做一些事情。

      首先,正如您所说,这里最会杀死您的事情(前提是您的数学函数足够简单)是分支预测。所以为了摆脱分支,我会创建一个函数指针数组,而不是做

      if(parameter == 1)
          function1();
      if...
      

      你可以这样做:

      array_of_functions[parameter]();
      

      这将摆脱所有分支预测并大大提高吞吐量,因为您的管道不必被刷新。编译器还应该能够内联函数。

      【讨论】:

        【解决方案5】:

        这取决于很多事情,但通常您可能希望这样做,以便在大多数情况下连续调用第一个或第二个函数。这将使现代 CPU 执行得更快(参见 Why is it faster to process a sorted array than an unsorted array?)。

        您可以使用数组和函数指针,但这可能不会加快速度,需要尝试。您可以使用http://www.boost.org/doc/libs/1_54_0/doc/html/function/tutorial.html#idp59212272 提供帮助,但静态函数不需要它。

        【讨论】:

          【解决方案6】:

          我认为最有效的方法之一是创建一个函数指针数组,然后您可以直接传递函数指针而不仅仅是参数。这将节省您在嵌套循环中使用 if/switch 语句所产生的任何类型的开销。

          举个例子:

          double expression_0(double a, double b) {...};
          double expression_1(double a, double b) {...};
          
          void function(double (*expression)(double, double)) {
              for (...) {
                  ...
                  double a = ...;
                  for (...) {
                      double b = ...;
                      double result = (*expression)(a, b);
                  }
              }
          }
          
          int main() {
              double (*fpointers[2]) (double, double);
              fpointers[0] = expression_0;
              fpointers[1] = expression_1;
          
              int parameter = ...;
              function(fpointers[parameter]);
          }
          

          【讨论】:

          • 函数调用是否应该比条件语句便宜?
          • 我敢肯定,在汇编级别,这应该至少比每次进行正确的分支预测更有效,因为没有条件要评估,而函数调用应该是相同的就像调用普通函数一样。
          【解决方案7】:

          如果您的所有函数都具有相同的签名,那么最简单的做法是:

          void function(int parameter)
          {
            double ( *fn )( double, double );
          
            switch( parameter )
            {
              case 1:  fn = &some_math_expression_1;  break;
              case 2:  fn = &some_math_expression_2;  break;
              ...
            }
          
            for( ... ) // a big loop
            {
              double a = ...;
              for( ... ) // a big loop
              {
                double b = ...;
                double value = fn( a, b );
                ...
              }
            }
          }
          

          【讨论】:

          • 我问@cheeyos,你认为函数调用比条件语句便宜吗?
          • 无论如何,您都在调用该函数。将条件移出循环将使您受益——具体多少将取决于太多的外部因素。值得注意的是,目标处理器上的分支预测缓存有多大,以及在数学函数中使用了多少。显然,减少你必须做的处理量很可能对性能产生比这样微不足道的影响更大的影响。
          • 显然更好的解决方案是去掉你的parameter 参数,只在你可以处理的任何地方处理函数指针。在每个函数内移动循环也可以让优化器更好地向量化和/或并行化实际处理代码 - 但这可能不像听起来那么容易,具体取决于您的具体情况。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-12-03
          • 2021-12-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多