为什么GCC聚合数组的初始化会首先用零填充整个东西,包括非零元素?


21

为什么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编译器资源管理器)。

# 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和with rep stosd 这是错过的优化,还是以这种方式更有效?(C实际上调用memset而不是内联rep stos


编者注:该问题最初具有未优化的编译器输出,其工作方式相同,但at处的低效率代码-O0无法证明任何事情。但事实证明,即使在时,GCC也错过了此优化-O3

将指针传递a给非内联函数将是迫使编译器实现的另一种方式a[],但是在32位代码中会导致汇编的大量混乱。(堆栈args会导致push,而push与存储中的数据混合到堆栈中以初始化数组。)

使用volatile a[100]{1,2,3,4}获取GCC来创建然后复制该数组,这是很疯狂的。通常,volatile这有助于查看编译器如何初始化局部变量或将其布置在堆栈上。


1
@Damien您误解了我的问题。我问为什么例如给a [0]赋值两次,就好像a[0] = 0;然后a[0] = 1;
Lassie

1
我无法读取该程序集,但是在哪里显示该数组完全用零填充?
smac89

3
另一个有趣的事实:对于更多已初始化的项目,gcc和clang都还原为从.rodata... 复制整个数组。
小丑

2
您禁用了优化;除非您验证相同的事情-O3(确实如此),否则低效的代码就不足为奇了。 godbolt.org/z/rh_TNF
彼得·科德斯

12
您还想知道什么?这是一项错过的优化,请使用missed-optimization关键字在GCC的bugzilla中进行报告。
彼得·科德斯

Answers:


2

理论上,您的初始化看起来像这样:

int a[100] = {
  [3] = 1,
  [5] = 42,
  [88] = 1,
};

因此从缓存和优化的角度出发,先将整个内存块清零,然后再设置各个值,可能会更有效。

行为可能取决于:

  • 目标架构
  • 目标操作系统
  • 数组长度
  • 初始化比率(显式初始化的值/长度)
  • 初始化值的位置

当然,在您的情况下,初始化是在数组的开头压缩的,而优化将是微不足道的。

因此,看来gcc在这里使用的是最通用的方法。看起来好像缺少优化。


是的,代码的最佳策略可能是将所有内容归零,或者仅仅是从a[6]尽头的空白开始填充所有立即数或零。特别是如果定位到x86-64,则可以使用qword存储一次执行2个元素,其中较低的一个为非零。例如mov QWORD PTR [rsp+3*4], 1用一个未对齐的qword存储来处理元素3和4。
Peter Cordes

从理论上讲,行为可能取决于目标操作系统,但在实际的GCC中则不会,也没有理由。仅目标体系结构(并且在其中,针对不同的微体系结构(如-march=skylakevs. -march=k8vs.)的调整选项-march=knl通常会大相径庭,也许就此而言应采用适当的策略。)
Peter Cordes

C ++甚至允许这样做吗?我认为这仅仅是C.
莱西

@Lassie您在C ++中是正确的,这是不允许的,但是问题与编译器后端更多相关,因此这无关紧要。所示的代码也可能都是
vlad_tepesch

您甚至可以通过声明一些内容struct Bar{ int i; int a[100]; int j;} 并初始化Bar a{1,{2,3,4},4};gcc进行相同的操作来轻松构造在C ++中工作相同的示例:将所有内容清零,然后设置5个值
vlad_tepesch
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.