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
但向循环中添加一些代码,您将获得相同的行为。
总而言之,从务实的开发人员的角度来看,将所有这些优化标志视为对编译器的提示。这总是一个权衡,当你构建一个软件时,你 永远不会 想要 all 或 no循环展开。
最后一点,另一个非常相似的例子是 -f(no-)inline-functions
标志。我每天都在努力让编译器内联(或不内联!)我的一些函数(使用 inline
关键字和 __attribute__ ((noinline))
与 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
但向循环中添加一些代码,您将获得相同的行为。
总而言之,从务实的开发人员的角度来看,将所有这些优化标志视为对编译器的提示。这总是一个权衡,当你构建一个软件时,你 永远不会 想要 all 或 no循环展开。
最后一点,另一个非常相似的例子是 -f(no-)inline-functions
标志。我每天都在努力让编译器内联(或不内联!)我的一些函数(使用 inline
关键字和 __attribute__ ((noinline))
与 GCC),当我检查汇编代码时,我看到这个聪明人有时仍然在做它想做的事,当我想内联一个对它的口味来说绝对太长的函数时。大多数时候,这是正确的做法,我很高兴!