GCC 中的循环展开行为

Loop unrolling behaviour in GCC

这个问题部分是 的跟进问题。

根据 GCC documentation,并且如我对上述问题的回答所述,-funroll-loops 等标志打开 "complete loop peeling (i.e. complete removal of loops with a small constant number of iterations)" .因此,当启用这样的标志时,如果编译器确定这将优化给定代码段的执行,则它可以选择展开循环。

尽管如此,我在我的一个项目中注意到 GCC 有时会展开循环 即使相关标志未启用 。例如,考虑以下简单代码:

int main(int argc, char **argv)
{
  int k = 0;
  for( k = 0; k < 5; ++k )
  {
    volatile int temp = k;
  }
}

当使用 -O1 编译时,循环被展开并且使用任何现代版本的 GCC 生成以下汇编代码:

main:
        movl    [=11=], -4(%rsp)
        movl    , -4(%rsp)
        movl    , -4(%rsp)
        movl    , -4(%rsp)
        movl    , -4(%rsp)
        movl    [=11=], %eax
        ret

即使在使用额外的 -fno-unroll-loops -fno-peel-loops 进行编译以确保标志被 禁用 时,GCC 意外地仍然对上述示例执行循环展开。

这一观察使我想到了以下密切相关的问题。为什么即使禁用了与此行为对应的标志,GCC 仍会执行循环展开?展开是否也由其他标志控制,即使 -funroll-loops 被禁用,这些标志也会使编译器在某些情况下展开循环?有没有办法在 GCC 中完全禁用循环展开(使用 -O0 编译的一部分)?

有趣的是,Clang 编译器在此处具有预期的行为,并且似乎仅在启用 -funroll-loops 时执行展开,而在其他情况下则不会。

在此先致谢,如有任何关于此事的其他见解,我们将不胜感激!

Why does GCC perform loop unrolling even though the flags corresponding to this behaviour are disabled?

从务实的角度考虑:将这样的标志传递给编译器时你想要什么?没有C++开发者会要求GCC展开或不展开循环,只是为了汇编代码中有或没有循环,这是有目标的。例如,如果您正在开发存储空间有限的嵌入式软件,-fno-unroll-loops 的目标是牺牲一点速度以减小二进制文件的大小。另一方面,-funrool-loops 的目标是告诉编译器您不关心二进制文件的大小,因此它应该毫不犹豫地展开循环。

但这并不意味着编译器会盲目展开或不展开所有循环!

在您的示例中,原因很简单:循环仅包含 一条 指令 - 在任何平台上都很少字节 - 编译器知道这是可以忽略不计的,并且无论如何都会花费几乎与循环所需的汇编代码大小相同(sub + mov + jne on x86-64)。

这就是为什么 gcc 6.2 和 -O3 -fno-unroll-loops 会变成这个代码:

int mul(int k, int j) 
{   
  for (int i = 0; i < 5; ++i)
    volatile int k = j;

  return k; 
}

...到下面的汇编代码:

 mul(int, int):
  mov    DWORD PTR [rsp-0x4],esi
  mov    eax,edi
  mov    DWORD PTR [rsp-0x4],esi
  mov    DWORD PTR [rsp-0x4],esi
  mov    DWORD PTR [rsp-0x4],esi
  mov    DWORD PTR [rsp-0x4],esi  
  ret    

它不会听你的,因为它(几乎,取决于体系结构)不会改变二进制文件的大小,但速度更快。但是,如果您增加一点循环计数器...

int mul(int k, int j) 
{   
  for (int i = 0; i < 20; ++i)
    volatile int k = j;

  return k; 
}

...它遵循您的提示:

 mul(int, int):
  mov    eax,edi
  mov    edx,0x14
  nop    WORD PTR [rax+rax*1+0x0]
  sub    edx,0x1
  mov    DWORD PTR [rsp-0x4],esi
  jne    400520 <mul(int, int)+0x10>
  repz ret 

如果将循环计数器保持在 5 但向循环中添加一些代码,您将获得相同的行为。

总而言之,从务实的开发人员的角度来看,将所有这些优化标志视为对编译器的提示。这总是一个权衡,当你构建一个软件时,你 永远不会 想要 allno循环展开。

最后一点,另一个非常相似的例子是 -f(no-)inline-functions 标志。我每天都在努力让编译器内联(或不内联!)我的一些函数(使用 inline 关键字和 __attribute__ ((noinline)) 与 GCC),当我检查汇编代码时,我看到这个聪明人有时仍然在做它想做的事,当我想内联一个对它的口味来说绝对太长的函数时。大多数时候,这是正确的做法,我很高兴!