如何从紧密循环中分解出分支?

时间:2011-09-27 14:05:14

标签: c++ performance theory

我的问题是:如何在我的处理循环中添加功能而不会检查用户设置的真/假以导航分支?循环上的所有迭代的设置都相同。具有分支预测的现代处理器是否使这不必要?

我的程序遵循这种模式:

  1. 用户可以调整设置,复选框,滑块,数字输入。
  2. 触发更新时处理数据
    1. 将设置应用于本地变量
    2. 在大型数据集上运行循环
      • 添加if语句以绕过用户设置中未使用的代码。
      • 从循环返回
    3. 返回转换后的数据
  3. 如何提前模板化或内联所有排列?

    示例:

    bool setting1 = true;
    bool setting2 = false;
    vector<float> data;
    
    for(int i=0;i<100000;i++)
        data.push_back(i);
    
    for(int i=0;i<100000;i++) {
        if (setting1)
        {
           doStuff(data[i]);
           ....
        }
        if (setting2)
        {
           doMoreStuff(data[i]);
           .....
        }
    
        .... //etc
    }
    

    我知道这是一个愚蠢的例子。但是,当有很多分支时,我想知道什么模式可以扩展。

4 个答案:

答案 0 :(得分:4)

使用主循环模板。

template <bool A, bool B>
void loop() {
  while (1) {
      if (A) // will get compiled out if A == false
      {
         doStuff(data[i]);
         ....
      }
      if (B)
      {
         doMoreStuff(data[i]);
         .....
      }

      .... //etc
  }
}

当您更改设置时:(您可能会减少此代码)

if (setting1) {
  if (setting2)
    loop<1,1>;
  else 
    loop<1,0>;
}
else {
  if (setting2)
    loop<0,1>;
  else 
    loop<0,0>;
}

您希望保持循环()直到设置更改。

应谨慎使用,否则会导致臃肿。


描述了答案(G ++ O2优化):

 %Time
 46.15      0.84     0.84                             fb() (blackbear)
 38.37      1.53     0.69                             fa() (OP)
 16.13      1.82     0.29                             fc() (pubby8)

答案 1 :(得分:2)

首先,除非操作与循环迭代(分支+循环开销)的成本相比非常便宜,否则不要担心它并做任何最可读的事情。过早优化是多恶的根源;不要只是假设事情会变慢,做一些分析,以便你知道

如果你确实发现自己真正花费更多的时间来迭代而不是做有用的工作 - 也就是说,你的开销太高 - 你需要找到一种合理的方法来减少开销;因此,要在为特定输入组合优化的不同循环体/实现之间进行选择。

将循环中的条件考虑在内,制作多个循环可能最初似乎是一个好主意,但是如果大多数设置大部分已启用并且您的实际操作足够便宜以使开销成为问题,那么您可能会发现性能基本不变 - 每个新循环都有每次迭代的成本!

如果是这种情况,前进的方法可能是使用模板或其他方法为最常见的输入组合实例化循环体的变体,在循环之间选择高级别,在合适的可用时调用它们,当它不是时,回到通用案例。

答案 2 :(得分:1)

您可以通过这种方式避免开销(假设settingx不影响设置):

if(setting1) {
    for(int i=0;i<100000;i++) {
        // ...
    }
}

if(setting3) {
    for(int i=0;i<100000;i++) {
        // ...
    }
}

if(setting3) {
    for(int i=0;i<100000;i++) {
        // ...
    }
}

但是,在我看来,最好的解决方案是保留您的代码。今天的分支预测单元功能非常强大,考虑到每个分支具有相同结果,您将循环数千次,您可以承受几个预热周期;)

修改
我用简单的控制台程序(sources比较了我们解决问题的方法,但它是c#)。循环执行1000000次,我使用三角函数和双精度浮点运算。测试2是我在上面显示的解决方案,三个字母是setting1,setting2和setting3的值 结果是:

test 1 - fft: 13974 ms
test 2 - fft: 14106 ms

test 1 - tft: 27728 ms
test 2 - tft: 28081 ms

test 1 - ttt: 41833 ms
test 2 - ttt: 41982 ms

我还做了一个测试运行,所有三个测试函数都为空以证明循环并且调用开销很小:

test 1 - fft: 4 ms
test 2 - fft: 4 ms

test 1 - tft: 8 ms
test 2 - tft: 8 ms

test 1 - ttt: 12 ms
test 2 - ttt: 12 ms

实际上,我的解决方案慢了约1%。我的答案的第二点被证明是正确的:循环开销是完全可以追踪的。

答案 3 :(得分:1)

如果在编译时已知数据集的大小,则编译器可能会执行:

  • 循环展开

如果是数学运算

  • 矢量化可以发挥作用

你也可以从里到外做逻辑:

if (epic-setting)
{
//massive for loop
}

对内存位置不利,正如一个人所说。

当且仅当错过的分支的成本低于给定的加速时,分支预测将帮助您获得好处(对于大型数据集,它应该有帮助,而不是伤害)。

如果您的数据操作是完全并行的,即您正在运行SIMD,则可以调查线程化操作:例如,打开3个线程,让所有3个线程执行i % t操作,{{1}作为线程索引,t是数据索引。 (您可以通过不同方式对数据进行分区)。对于足够大的数据集,假设您没有同步操作,您将看到线性加速。

如果您是为专门的系统编写此文件,例如具有给定CPU的工业计算机,并且您可以假设您将始终拥有该CPU,则可以针对CPU可以支持的内容进行更大程度的优化。像确切的缓存大小,管道深度等等都可以编码。除非你能想到这一点,否则在这些方面尝试并假设是粗略的。