5

取自 GCC 手册:

-funroll-loops
           Unroll loops whose number of iterations can be determined at compile time or upon entry to the loop.
           -funroll-loops implies -frerun-cse-after-loop.  This option makes code larger, and may or may not make it
           run faster.

根据我的理解,展开循环将摆脱结果代码中的分支指令,我认为它对 CPU 管道更健康。

但为什么它“可能不会让它跑得更快”呢?

4

6 回答 6

7

首先,它可能没有任何区别;如果你的条件是“简单的”并且执行了很多次,那么分支预测器应该快速拾取它并始终正确预测分支直到循环结束,使“滚动”代码的运行速度几乎与展开代码一样快。

此外,在非流水线 CPU 上,分支的成本非常小,因此此类优化可能无关紧要,代码大小考虑可能更为重要(例如,在为微控制器编译时 - 请记住 gcc 目标范围从 AVR 微型计算机到超级计算机)。

展开不能加速循环的另一种情况是循环体比循环本身慢得多 - 例如,如果在体循环中有一个系统调用,那么与系统调用相比,循环开销可以忽略不计。

至于它何时可能会使您的代码运行速度变慢,使代码变大会减慢它的速度 - 如果您的代码不再适合缓存/内存页面/...您将遇到缓存/页面/...故障并且处理器在执行之前必须等待内存获取代码。

于 2013-06-17T23:05:45.977 回答
1

到目前为止的答案都很好,但我将添加一件尚未涉及的事情:吃掉分支预测器插槽。如果你的循环包含一个分支,并且它没有展开,它只消耗一个分支预测器槽,所以它不会驱逐 CPU 在外部循环、姐妹循环或调用者中所做的其他预测。但是,如果循环体通过展开多次复制,则每个副本将包含一个单独的分支,该分支消耗一个预测器槽。这种性能影响很容易被忽视,因为就像缓存驱逐问题一样,它在大多数孤立的、人工的循环性能测量中是不可见的。相反,它将表现为损害其他代码的性能。

作为一个很好的例子,strlenx86 上最快的(甚至比我见过的最好的 asm 还要好)是一个疯狂展开的循环,它简单地做了:

if (!s[0]) return s-s0;
if (!s[1]) return s-s0+1;
if (!s[2]) return s-s0+2;
/* ... */
if (!s[31]) return s-s0+31;

但是,这会撕裂分支预测器槽,因此出于实际目的,某种矢量化方法是可取的。

于 2013-06-18T01:48:15.037 回答
1

我认为用条件退出填充展开的循环并不常见。这打破了展开允许的大部分指令调度。n更常见的是在进入展开部分之前预先检查循环是否至少剩余迭代。

为了实现这一点,编译器可以生成精细的前同步码和后同步码以对齐循环数据以实现更好的矢量化或更好的指令调度,并处理未均匀划分为循环展开部分的剩余迭代。

结果可能是(最坏的情况)循环只运行零次或一次,或者在特殊情况下可能运行两次。然后只执行循环的一小部分,但要执行许多额外的测试才能到达那里。更差; 对齐前导码可能意味着在不同的调用中会出现不同的分支条件,从而导致额外的分支错误预测停顿。

这些都是为了抵消大量迭代,但对于短循环,这不会发生。

最重要的是,您的代码大小增加了,所有这些展开的循环一起有助于降低 icache 效率。

并且一些架构特殊情况下非常短的循环使用它们的内部缓冲区,甚至不参考缓存。

并且现代架构具有相当广泛的指令重新排序,甚至围绕内存访问,这意味着即使在最好的情况下,编译器对循环的重新排序也可能不会提供额外的好处。

于 2013-06-18T04:23:51.417 回答
0

例如,展开的函数体大于缓存。从内存中读取显然更慢。

于 2013-06-17T23:05:36.140 回答
0

假设您有一个包含 25 条指令的循环并迭代 1000 次。处理 25,000 条指令所需的额外资源可以很好地克服分支带来的痛苦。

同样重要的是要注意,许多循环分支都非常轻松,因为 CPU 在更简单情况下的分支预测方面已经非常出色。例如,展开 8 次迭代可能更有效,但即使是 50 次也可能最好留给 CPU。请注意,编译器可能更擅长猜测哪个比你更好。

于 2013-06-17T23:06:19.167 回答
-1

展开循环应该总是使代码更快。权衡是在更快的代码和更大的代码占用空间之间。执行很多次的紧密循环(在循环体中执行的代码相对较少)可以通过消除所有循环开销并允许流水线完成其工作而受益于展开。经过多次迭代的循环可能会展开到大量额外代码 - 速度更快,但性能增益可能会更大的占用空间。体内发生很多事情的循环可能不会从展开中显着受益 - 与其他所有内容相比,循环开销变得很小。

于 2013-06-17T23:12:40.830 回答