【问题标题】:Why does GCC aggregate initialization of an array fill the whole thing with zeros first, including non-zero elements?为什么 GCC 聚合初始化数组首先用零填充整个事物,包括非零元素?
【发布时间】: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


【解决方案1】:

理论上你的初始化可能是这样的:

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

因此,首先将整个内存块清零,然后设置单独的值,在缓存和可优化性方面可能更有效。

可能是行为改变取决于:

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

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

所以看起来 gcc 在这里做的是最通用的方法。看起来缺少优化。

【讨论】:

  • 是的,this 代码的最佳策略可能是将所有内容归零,或者可能只是从 a[6] 开始的所有内容,早期空白处填充单个立即数存储或零。特别是如果以 x86-64 为目标,那么您可以使用 qword 存储一次执行 2 个元素,其中较低的一个非零。例如mov QWORD PTR [rsp+3*4], 1 使用一个未对齐的 qword 存储来执行元素 3 和 4。
  • 行为理论上可能取决于目标操作系统,但在实际的 GCC 中它不会,也没有理由这样做。只有目标架构(在此范围内,针对不同微架构的调整选项,例如 -march=skylake-march=k8-march=knl 总体上都会有很大不同,并且可能在适当的策略方面。)
  • C++ 中是否允许这样做?我以为只有C。
  • @Lassie 你在 c++ 中是对的,这是不允许的,但问题与编译器后端更相关,所以没关系。显示的代码也可以是
  • 您甚至可以通过声明一些 struct Bar{ int i; int a[100]; int j;} 和初始化 Bar a{1,{2,3,4},4}; 轻松构建在 C++ 中工作相同的示例 gcc 执行相同的操作:全部归零,然后设置 5 个值
猜你喜欢
  • 2012-09-01
  • 2023-01-31
  • 2015-12-21
  • 2017-04-17
  • 1970-01-01
  • 1970-01-01
  • 2016-12-17
  • 2021-11-22
  • 2016-10-05
相关资源
最近更新 更多