【发布时间】:2020-03-20 04:11:45
【问题描述】:
为什么 gcc 用零填充整个数组,而不是只填充剩余的 96 个整数?非零初始化器都在数组的开头。
void *sink;
void bar() {
int a[100]{1,2,3,4};
sink = a; // a escapes the function
asm("":::"memory"); // and compiler memory barrier
// forces the compiler to materialize a[] in memory instead of optimizing away
}
MinGW8.1 和 gcc9.2 都制作这样的 asm (Godbolt compiler explorer)。
# gcc9.2 -O3 -m32 -mno-sse
bar():
push edi # save call-preserved EDI which rep stos uses
xor eax, eax # eax=0
mov ecx, 100 # repeat-count = 100
sub esp, 400 # reserve 400 bytes on the stack
mov edi, esp # dst for rep stos
mov DWORD PTR sink, esp # sink = a
rep stosd # memset(a, 0, 400)
mov DWORD PTR [esp], 1 # then store the non-zero initializers
mov DWORD PTR [esp+4], 2 # over the zeroed part of the array
mov DWORD PTR [esp+8], 3
mov DWORD PTR [esp+12], 4
# memory barrier empty asm statement is here.
add esp, 400 # cleanup the stack
pop edi # and restore caller's EDI
ret
(启用 SSE 后,它将使用 movdqa 加载/存储复制所有 4 个初始化程序)
为什么 GCC 不像 Clang 那样只对最后 96 个元素执行 lea edi, [esp+16] 和 memset(使用 rep stosd)? 这是一个错过的优化,还是以某种方式更有效这样做? (Clang 实际上调用memset 而不是内联rep stos)
编者注:该问题最初具有未优化的编译器输出,其工作方式相同,但-O0 的低效代码并不能证明任何事情。但事实证明,即使在-O3,GCC 也错过了这种优化。
将指向a 的指针传递给非内联函数将是强制编译器实现a[] 的另一种方法,但在32 位代码中会导致asm 非常混乱。 (堆栈参数导致推送,它与存储混合到堆栈中以初始化数组。)
使用volatile a[100]{1,2,3,4} 让GCC 创建然后复制 数组,这太疯狂了。通常volatile 非常适合查看编译器如何初始化局部变量或将它们放置在堆栈上。
【问题讨论】:
-
@Damien 你误解了我的问题。我问为什么例如 a[0] 被赋值两次,就像
a[0] = 0;然后a[0] = 1;。 -
我无法读取程序集,但它在哪里显示数组完全用零填充?
-
另一个有趣的事实:对于更多初始化的项目,gcc 和 clang 都恢复为从
.rodata复制整个数组......我不敢相信复制 400 个字节比归零和设置 8 个项目更快. -
您禁用了优化;除非您验证在
-O3上发生了同样的事情(确实如此),否则效率低下的代码并不令人惊讶。 godbolt.org/z/rh_TNF -
您还想了解什么?这是一个错过的优化,请在 GCC 的 bugzilla 上使用
missed-optimization关键字报告它。
标签: c++ gcc assembly x86 compiler-optimization