【问题标题】:How can you factor out branching from tight loop?您如何从紧密循环中排除分支?
【发布时间】:2011-11-26 02:18:06
【问题描述】:

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

我的程序遵循这种模式:

  1. 用户调整设置、复选框、滑块、数字条目。
  2. 触发更新时处理数据
    1. 将设置应用于局部变量
    2. 在大型数据集上运行循环
      • 添加 if 语句以绕过用户设置中未使用的代码。
      • 从循环返回
    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
}

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

【问题讨论】:

  • 你为什么要问?您的循环是否存在性能问题?你量过吗? (如果没有,编写最容易维护的代码,并在分析显示您需要时对其进行优化,使用分析作为最佳优化指南。)
  • 也许你应该首先选择清洁而不是性能。除了设置,您还可以使用 vectorstd::functionS,这需要根据设置在迭代中执行。在其他语言中称为hooks。它非常易于维护且易于阅读。
  • 我对反馈感到满意。
  • 如果有人可以“模板化”分支调度功能,则可以获得奖励积分。例如。 if (setting1) 循环();否则循环();

标签: c++ performance theory


【解决方案1】:

在主循环中使用模板。

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>;
}

你想留在 loop() 直到设置改变。

这应该小心使用,因为它会导致膨胀。


分析答案(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)

【讨论】:

  • 由此产生的唯一“膨胀”是循环的扩散——这正是 OP 想要的。整个模板魔法是纯编译时的,编译后应该完全消除。
  • @sbi 如果你有 16 个设置,你会突然创建 256 个函数。如果你不小心,它可能会导致更糟糕的缓存局部性。
  • 当然可以,但我的意思是,如果这些函数是手工编写的,那也是一样的。模板与它无关,只是它们使您不必手动编写和维护 256 个非常相似的函数。
  • @JamesKanze:这里肯定有一个严重的误解,但我看不到你有任何努力来澄清这一点。我所看到的只是反复断言你是对的,我是错的。由于这里没有什么可以向我学习的,所以我放弃了这个讨论。手。
  • @JamesKanze 我不明白你在争论什么。您必须编写与 1 相对的 2 个函数,但这样做的优化好处是值得的。
【解决方案2】:

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

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

将条件排除在循环之外,进行多个循环,最初似乎是个好主意,但是如果大多数设置大部分都已启用并且您的实际操作足够便宜,可以完全解决开销问题,那么您可能会发现性能基本没有变化 - 每个新循环都有每次迭代成本!

如果是这种情况,前进的方法可能是使用模板或其他方法来实例化循环体的变体,以获得最常见的输入组合,在循环之间进行高级别选择,当合适的循环可用时调用那些循环体,如果不是,则回退到一般情况。

【讨论】:

    【解决方案3】:

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

    • 循环展开

    如果是数学运算

    • 矢量化可以发挥作用

    你也可以由内而外地做逻辑:

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

    正如一个人所说,它对内存局部性不利。

    当且仅当丢失分支的成本小于给定的加速比时,分支预测将为您提供很大帮助(对于大型数据集,它应该有所帮助,而不是伤害)。

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

    如果您是为专门的系统编写此代码,例如具有给定 CPU 的工业计算机,并且您可以假设您将始终拥有该 CPU,那么您可以针对该 CPU 可以支持的功能进行更多优化。确切的缓存大小、管道深度等都可以编码。除非您可以假设,否则尝试假设这些计数是粗略的。

    【讨论】:

      【解决方案4】:

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

      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%。我的回答的第二点,虽然被证明是正确的:循环开销是完全可追溯的。

      【讨论】:

      • 这对于内存局部性来说非常糟糕
      • @yi_H:好吧,虽然你是对的,但否决票是不公平的,因为我已经正确回答了 OP 的问题,“我怎样才能在我的处理循环中添加功能 检查用户设置的真/假以导航分支的开销“
      • @BlackBear:反对票是我的。 IMO 句子“具有分支预测的现代处理器是否使这变得不必要?”非常清楚地表明这是关于 性能 开销,而您的回答会以最糟糕的方式之一损害性能。
      • @sbi:我还是不明白,也许我没有解释清楚。基本上我的回答是:“具有分支预测的现代处理器是否不需要这样做?” ->“是”。我知道我提供的 sn-p 很愚蠢:)
      • 这里唯一的错误是没有组合设置组合。不这样做可能会导致循环遍历所有数据多次。但是,如果组合的数量太大,则代码变得难以管理,每个设置组合都有一个循环。
      猜你喜欢
      • 1970-01-01
      • 2011-01-13
      • 1970-01-01
      • 2019-09-09
      • 2010-11-03
      • 1970-01-01
      • 1970-01-01
      • 2011-04-24
      相关资源
      最近更新 更多