在GCC中循环展开行为

Pyv*_*ves 9 c++ gcc compiler-optimization loop-unrolling

这个问题部分是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;
  }
}
Run Code Online (Sandbox Code Playgroud)

在编译时-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
Run Code Online (Sandbox Code Playgroud)

即使在使用附加-fno-unroll-loops -fno-peel-loops程序进行编译以确保禁用标志时,GCC仍然意外地仍在上述示例中执行循环展开.

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

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

在此先感谢您对此事的任何其他见解将不胜感激!

Ant*_*cus 9

为什么GCC执行循环展开,即使禁用了与此行为相对应的标志?

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

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

在你的例子中,原因很简单:循环只包含一条指令 - 任何平台上的几个字节 - 并且编译器知道这是可以忽略的,并且无论如何都将采用与循环所需的汇编代码几乎相同的大小(sub+ mov+ jneon 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; 
}
Run Code Online (Sandbox Code Playgroud)

...到以下汇编代码:

 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    
Run Code Online (Sandbox Code Playgroud)

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

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

  return k; 
}
Run Code Online (Sandbox Code Playgroud)

...它遵循你的提示:

 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 
Run Code Online (Sandbox Code Playgroud)

如果你保持循环计数器5但是你在循环中添加了一些代码,你会得到相同的行为.

总而言之,将所有这些优化标志视为编译器的提示,并从实用的开发人员的角度出发.它始终是一种权衡,当您构建软件时,您永远不会要求全部不需要循环展开.

最后一点,另一个非常相似的例子就是-f(no-)inline-functions旗帜.我每天都在为编译器内联(或不是!)我的一些函数(使用inline关键字和__attribute__ ((noinline))GCC)而战斗,当我检查汇编代码时,我看到这个smartass有时仍在做它想要的东西,当我想要内联一个绝对太长的功能.大多数时候,这是正确的事情,我很高兴!