GCC中的循环展开行为

Loop unrolling behaviour in GCC

本文关键字:循环展开 GCC      更新时间:2023-10-16

这个问题在一定程度上是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时才执行展开,而在其他情况下则不执行。

提前感谢,任何关于此事的额外见解将不胜感激!

为什么GCC执行循环展开,即使标志对应的这个行为被禁用了吗?

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

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

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

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

最后要说明的是,另一个非常相似的例子是-f(no-)inline-functions标志。我每天都在与编译器内联(或不!)我的一些函数(使用inline关键字和GCC的__attribute__ ((noinline)))作斗争,当我检查汇编代码时,我看到这个聪明的家伙有时仍然在做它想做的事情,当我想内联一个函数时,它肯定太长了。大多数时候,这是正确的事情,我很高兴!