此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生成 更多 迭代。我错过了什么?
答案 0 :(得分:4)
是的,有些情况下循环展开会使代码更有效率。
该理论减少了开销(分支到循环顶部和递增循环计数器)。
大多数处理器讨厌分支指令。他们喜欢数据处理说明。对于每次迭代,至少有一个分支指令。通过&#34;复制&#34;一组代码,减少了分支的数量,并且在分支之间增加了数据处理指令。
许多现代编译器都有优化设置来执行循环展开。
答案 1 :(得分:0)
它不会产生更多的迭代;你会注意到调用g()
两次的循环运行的次数减少了一半。 (如果你必须拨打g()
奇数次怎么办?查看Duff的设备。)
在您的商家信息中,您会注意到汇编语言说明jne 8 <main+0x8>
在两者中都出现过一次。这告诉处理器返回循环的开始。在原始循环中,该指令将运行99次。在滚动循环中,它只运行49次。想象一下,如果循环体是非常短的,只需要一两条指令。这些跳转可能是程序中性能最关键部分的三分之一甚至一半的指令! (甚至还有一个有用的循环零指令:BogoMIPS。但关于优化的文章是一个笑话。)
因此,展开循环会交换代码大小的速度,对吧?没那么快。也许你已经使你的展开循环变得如此之大,以至于循环顶部的代码不再在缓存中,并且CPU需要获取它。在现实世界中,了解它是否有帮助的唯一方法是分析您的程序。