在GCC中循环展开行为

时间:2016-09-13 20:04:46

标签: c++ gcc compiler-optimization loop-unrolling

这个问题部分是GCC 5.1 Loop unrolling的后续问题。

根据GCC documentation,并且如我对上述问题的回答所述,-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时执行展开,而不是在其他情况下执行。

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

1 个答案:

答案 0 :(得分:9)

  

为什么GCC即使执行标记也会执行循环展开   对应这种行为是禁用的吗?

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

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

在你的例子中,原因很简单:循环只包含一个指令 - 在任何平台上都有几个字节 - 并且编译器知道这是可以忽略的,并且无论如何都将采用与循环所需的汇编代码(sub + mov + jne 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但是在循环中添加了一些代码,则会得到相同的行为。

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

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