【问题标题】:In which scenario would "unroll-loops" not making result code faster?在哪种情况下“展开循环”不会使结果代码更快?
【发布时间】:2013-06-17 23:00:00
【问题描述】:

取自 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 管道更健康。

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

【问题讨论】:

  • 你的代码什么时候没有循环?
  • 当循环体较大时,循环控制开销微不足道,而且由于展开较大的代码大小会导致指令缓存未命中。
  • 在大多数大型应用程序中,循环展开几乎总是会导致代码变慢。它与缓存一起使用。当然,只有分析器会告诉您它是否适用于您的特定应用程序。

标签: c


【解决方案1】:

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

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

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

至于何时它可能会使您的代码运行速度变慢,增大代码会使其变慢 - 如果您的代码不再适合缓存/内存页面/...您将拥有一个缓存/页面/。 .. 错误,处理器将不得不等待内存在执行之前获取代码。

【讨论】:

    【解决方案2】:

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

    作为一个很好的例子,x86 上最快的strlen(甚至比我见过的最好的 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;
    

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

    【讨论】:

      【解决方案3】:

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

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

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

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

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

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

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

      【讨论】:

        【解决方案4】:

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

        【讨论】:

          【解决方案5】:

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

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

          【讨论】:

            【解决方案6】:

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

            【讨论】:

            • 它不会总是更快。它在很大程度上取决于特定的发出代码和处理器/硬件。
            • “移除所有循环开销,并允许流水线完成它的工作”——这是一个误解;见en.wikipedia.org/wiki/Software_pipelining
            • 也许有更好的方法我可以写出来
            • 我特别关注这个问题,因为展开会使事情变得变慢
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-09-21
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多