【问题标题】:Does this optimization even matter?这种优化甚至重要吗?
【发布时间】:2015-11-30 23:19:33
【问题描述】:

page 推荐“循环展开”作为优化:

可以通过减少迭代次数来减少循环开销 复制循环体。

示例:

在下面的代码片段中,可以复制循环体 一次,迭代次数可以从100次减少到50次。

for (i = 0; i < 100; i++)
  g ();

下面是循环展开后的代码片段。

for (i = 0; i < 100; i += 2)
{
  g ();
  g ();
}

在 GCC 5.2 中,除非您使用 -funroll-loops(在 -O2-O3 中未启用),否则不会启用循环展开。我检查了组件,看看是否有显着差异。

g++ -std=c++14 -O3 -funroll-loops -c -Wall -pedantic -pthread main.cpp && objdump -d main.o

版本 1:

   0:   ba 64 00 00 00          mov    $0x64,%edx
   5:   0f 1f 00                nopl   (%rax)
   8:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # e <main+0xe>
   e:   83 c0 01                add    $0x1,%eax
  # ... etc ...
  a1:   83 c1 01                add    $0x1,%ecx
  a4:   83 ea 0a                sub    $0xa,%edx
  a7:   89 0d 00 00 00 00       mov    %ecx,0x0(%rip)        # ad <main+0xad>
  ad:   0f 85 55 ff ff ff       jne    8 <main+0x8>
  b3:   31 c0                   xor    %eax,%eax
  b5:   c3                      retq

版本 2:

   0:   ba 32 00 00 00          mov    $0x32,%edx
   5:   0f 1f 00                nopl   (%rax)
   8:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # e <main+0xe>
   e:   83 c0 01                add    $0x1,%eax
  11:   89 05 00 00 00 00       mov    %eax,0x0(%rip)        # 17 <main+0x17>
  17:   8b 0d 00 00 00 00       mov    0x0(%rip),%ecx        # 1d <main+0x1d>
  1d:   83 c1 01                add    $0x1,%ecx
  # ... etc ...
 143:   83 c7 01                add    $0x1,%edi
 146:   83 ea 0a                sub    $0xa,%edx
 149:   89 3d 00 00 00 00       mov    %edi,0x0(%rip)        # 14f <main+0x14f>
 14f:   0f 85 b3 fe ff ff       jne    8 <main+0x8>
 155:   31 c0                   xor    %eax,%eax
 157:   c3                      retq 

版本 2 产生 更多 次迭代。我错过了什么?

【问题讨论】:

  • 是否愿意发布该代码的源代码以获得更好的答案?
  • IMO,最好把它留给编译器来展开循环。甚至英特尔都表示最好保持循环展开。 software.intel.com/en-us/articles/avoid-manual-loop-unrolling
  • 我认为这个例子是为了展示这个想法。像本例那样花费开发时间优化代码是非常昂贵的。
  • 示例 2 如何产生更多的迭代?循环计数器加 2,因此循环执行次数减半。编译器可能复制了循环中的代码,但没有增加迭代次数。 迭代和循环代码内容的巨大区别.
  • 唯一的方法是分析你的代码。循环展开可能会有所帮助,因为它减少了跳转次数,或者可能会因为较大的循环体不再全部在缓存中而造成伤害。

标签: c++ optimization g++ compiler-optimization loop-unrolling


【解决方案1】:

是的,在某些情况下,循环展开会使代码更有效率。

理论是减少较少的开销(分支到循环顶部并递增循环计数器)。

大多数处理器讨厌分支指令。他们喜欢数据处理指令。对于每次迭代,至少有一个分支指令。通过“复制”一组代码,减少了分支的数量,增加了分支之间的数据处理指令。

许多现代编译器都有优化设置来执行循环展开。

【讨论】:

  • 对于具有分支预测器、推测执行和指令缓存的现代 CPU,这并不是一幅完整的图景。如果有的话,循环展开的收益正在稳步下降。不过,这与此答案中的任何内容都没有矛盾。
  • 基本理论是数据处理指令比分支指令处理得更快。正如您所说,分支指令会导致分支预测启动,这通常比数据处理周期慢;它确实加快了分支处理周期。
【解决方案2】:

它不会产生更多的迭代;你会注意到调用g() 两次的循环运行了一半的次数。 (如果你必须给g()打奇数次电话怎么办?查查达夫的设备。)

在您的列表中,您会注意到汇编语言指令 jne 8 &lt;main+0x8&gt; 在两者中都出现一次。这告诉处理器回到循环的开始。在原始循环中,这条指令将运行 99 次。在滚动循环中,它只会运行 49 次。想象一下,如果循环体很短,只有一两条指令。这些跳转可能是程序中对性能最关键的部分中指令的三分之一甚至一半! (甚至还有一个带有 zero 指令的有用循环:BogoMIPS。但是关于优化的文章是个笑话。)

所以,展开循环会以速度换取代码大小,对吧?没那么快。也许你把展开的循环做得很大,以至于循环顶部的代码不再在缓存中,CPU 需要获取它。在现实世界中,了解它是否有帮助的唯一方法是分析您的程序。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-26
    • 2013-07-18
    • 2019-11-22
    • 1970-01-01
    相关资源
    最近更新 更多