GCC中的循环展开行为

11
这个问题部分是关于GCC 5.1 循环展开的后续问题。
根据GCC文档和我对上述问题的回答,像-funroll-loops这样的标志会打开"完整的循环剥离(即完全删除少量迭代的循环)"。因此,当启用该标志时,编译器可以选择展开一个循环,如果它确定这将优化给定代码的执行。
尽管如此,我注意到在我的项目中,即使没有启用相关标志,GCC有时也会展开循环。例如,请考虑以下简单的代码片段:
int main(int argc, char **argv)
{
  int k = 0;
  for( k = 0; k < 5; ++k )
  {
    volatile int temp = k;
  }
}

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

main:
        movl    $0, -4(%rsp)
        movl    $1, -4(%rsp)
        movl    $2, -4(%rsp)
        movl    $3, -4(%rsp)
        movl    $4, -4(%rsp)
        movl    $0, %eax
        ret
即使在编译时使用了附加的 -fno-unroll-loops -fno-peel-loops,以确保标志被禁用,GCC仍然意外地对上述示例进行循环展开。
这个观察结果引出以下密切相关的问题。为什么GCC会执行循环展开,即使相应于该行为的标志已被禁用?循环展开是否也受其他标志的控制,这些标志可以使编译器在某些情况下展开循环,即使-funroll-loops 已禁用?有没有一种方法可以完全禁用GCC中的循环展开(除了使用-O0 进行编译)?
有趣的是,Clang编译器在这里具有期望的行为,似乎只在启用 -funroll-loops时才执行展开,而不是在其他情况下。
提前感谢,任何关于此事的额外见解将不胜感激!

你的程序功能会被破坏吗? - Serge
2
不,它不会破坏功能。这更多是一个关于GCC如何执行循环展开以及如何调整此行为的一般性问题。 - Pyves
1个回答

13
为什么即使对应于此行为的标志已被禁用,GCC仍执行循环展开? 考虑一下实际情况:当将此标志传递给编译器时,您想要什么? 没有任何C ++开发人员会要求GCC展开或不展开循环,仅仅是为了在汇编代码中有或没有循环,而是有一个目标。例如,在开发具有有限存储空间的嵌入式软件时,使用 -fno-unroll-loops 的目标是为了牺牲一点速度以减小二进制文件的大小。另一方面,使用 -funrool-loops 的目的是告诉编译器,您不关心二进制文件的大小,因此它不应该犹豫地对循环进行展开。

但这并不意味着编译器会盲目地展开或不展开您所有的循环!
在您的示例中,原因很简单:循环仅包含一个指令,对于任何平台都只有几个字节,并且编译器知道这是微不足道的,无论如何它所需的汇编代码的大小与循环(sub + mov + jne)几乎相同。

这就是为什么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,同时向循环中添加一些代码,您将获得相同的行为。

总之,将所有这些优化标志视为编译器的提示,并从务实的开发者角度考虑。这始终是一个权衡,在构建软件时,您永远不希望要求完全或不循环展开。

最后需要注意的是,另一个非常类似的示例是-f(no-)inline-functions标志。我每天都在与编译器斗争,以便内联(或不内联!)我的某些函数(GCC中使用inline关键字和__attribute__((noinline))),当我检查汇编代码时,我发现这个自作聪明的编译器有时仍然会按照自己的喜好进行操作,当我想要内联一个明显过长的函数时。大多数时候,这是正确的做法,我感到很高兴!


至少编译器通常会听取__attribute__ (((no)inline))和类似快速/严格数学的东西。我无法想象一个编译器会忽略一个严格数学标志。 - Mysticial
只是为了在汇编代码中有循环或没有循环,除了我:P - flaviut

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接