Newshot
Account

零初始化的那些花招

Translation(model translation, not the source's own text)

零初始化的怪事

这能有多难呢?

假设我们有:


        struct S {
    char buf[9u * 1024];
};

struct S
a(void)
{
    return (struct S){0};
}
      

a() 编译后变成:


        a:
    sub  rsp, 8
    mov  edx, 9216
    xor  esi, esi
    call memset
    add  rsp, 8
    ret
      

编译器为了零初始化生成了一个 memset() 调用。9K 似乎是 GCC 生成 memset() 而不是 rep 之类指令的阈值。不知道为什么,考虑到后者(我认为)是微码实现的,而向量操作在任何 N 下可能都更好。

不管怎样,上面的汇编仍然很有趣。在 SysV ABI 下,a() 被转换为:


        void
b(struct S *p)
{
    memset(p, 0, sizeof(*p));
}
      

确实,如果你编译它,会得到一模一样的* 汇编。调用者为返回值分配内存,并把指针传给被调用者。

如果你想知道的话,这和 C++ 的 RVO 完全无关。

现在,如果你擅长汇编(我不擅长),你会注意到编译器生成的不是最优的。编译手动“转换”后的 b(),你会得到:


        b:
    mov edx, 9216
    xor esi, esi
    jmp memset
      

这大概是最优的。哈,编译器对 memset() 调用做了 TCO。为什么它对 a() 做不到呢?如果你查看 LLVM 的 IR(在 -O3 下):


        define dso_local void @a(ptr dead_on_unwind noalias nofree writable writeonly sret(%struct.S) align 1 captures(none) initializes((0, 9216)) %agg.result) local_unnamed_addr {
entry:
  tail call void @llvm.memset.p0.i64(ptr noundef nonnull align 1 dereferenceable(9216) %agg.result, i8 0, i64 9216, i1 false)
  ret void
}

define dso_local void @b(ptr nofree noundef writeonly captures(none) initializes((0, 9216)) %s) local_unnamed_addr {
entry:
  tail call void @llvm.memset.p0.i64(ptr noundef nonnull align 1 dereferenceable(9216) %s, i8 0, i64 9216, i1 false)
  ret void
}
      

中端确实在 a() 里成功尾调用了 memset()!除了一些属性差异,两个函数降低后非常相似。所以实际上是后端出于某种原因搞砸了,没有生成尾调用。

GCC 也失败了,我不确定原因(ABI 脑损伤?)。不过,这并不重要,因为没人会那样写代码,而且调用反正会被内联。

哦,我亲爱的 GCC

在折腾这个的时候,我发现了一个很酷的 GCC bug。我是个老古董,用 Debian 13,它自带 GCC 14。如果你用那个编译一个 c() 变体,会得到:


        struct S
c(void)
{
    struct S r;
    memset(&r, 0, sizeof(r));
    return r;
}
      

        c:
    push rbx
    mov  edx, 9216
    mov  rbx, rdi
    xor  esi, esi
    sub  rsp, 9216
    mov  rdi, rsp
    call memset
    mov  rdi, rbx
    mov  edx, 9216
    xor  esi, esi
    call memset
    add  rsp, 9216
    pop  rbx
    ret
      

GCC 是不是很贴心?它决定格外小心,调用了两次 memset(),就为了确保无误。而且它从被调用者的栈帧里分配了一个 struct S。GCC 15 及以后则很鲁莽,只生成一个 memset() 调用,于是吐出的汇编和 a() 的相同。

但即使是用 GCC 15,似乎也漏掉了一些边缘情况,你仍然能得到疯狂的输出。如果我们有这样一个变体:


        struct S
d(void)
{
    struct S r = {0};
    return r;
}
      

        .LC0:
.zero 2
.zero 9214

d:
    push rbx
    mov  edx, 9216
    mov  rbx, rdi
    mov  esi, OFFSET FLAT:.LC0
    sub  rsp, 9216
    mov  rdi, rsp
    call memcpy
    mov  rsi, rsp
    mov  rdi, rbx
    mov  edx, 9216
    call memcpy
    add  rsp, 9216
    pop  rbx
    ret
      

它没有调用 memset(),而是创建了一个填满 0 的 9K 对象,然后做 memcpy()。两次。它从被调用者分配了一个 struct S r,把 9K 对象拷贝进去,然后把 r 拷贝到调用者分配的 struct S 里。什么鬼?

双重 memcpy() 在 GCC 16 中修复了,但仍然拷贝了一个 9K 对象,而不是调用 memset():


        .LC0:
.zero 2
.zero 9214
      

d: sub rsp, 8 mov edx, 9216 mov esi, OFFSET FLAT:.LC0 call memcpy add rsp, 8 ret


        
我想这个问题在 trunk 里已经修复了?这肯定是一个优化 bug,而不是正确性 bug。再来一个:

      

struct S e(void) { struct S r = (struct S){0}; return r; }


        
上面的代码与 d() 类似,但使用复合字面量来初始化 r(像 a() 那样)。GCC 16 为此生成 memset(),与 a() 的汇编相同。更早的版本也是如此。搞什么鬼?

如果使用 0 以外的值,仍然可能触发问题:

      

struct S f(void) { return (struct S){{-1}}; }


        
      

f: sub rsp, 8 mov edx, 9216 xor esi, esi call memset mov BYTE PTR [rax], -1 add rsp, 8 ret


        
      

struct S g(void) { struct S r = {{-1}}; return r; }


        
      

.LC0: .string "\377" .zero 9214

g: sub rsp, 8 mov edx, 9216 mov esi, OFFSET FLAT:.LC0 call memcpy add rsp, 8 ret


        
      

struct S h(void) { struct S r = (struct S){{-1}}; return r; }


        
      

h: push rbx mov edx, 9216 mov rbx, rdi xor esi, esi sub rsp, 9216 mov rdi, rsp call memset mov rsi, rsp mov rdi, rbx mov edx, 9216 mov BYTE PTR [rsp], -1 call memcpy add rsp, 9216 pop rbx ret

The translation is machine-generated and individual terms may be off. Nothing is rewritten or cut; the original is authoritative.

Published by Lobsters
This site only summarises and aggregates; copyright stays with the original author. Headlines and summaries follow the source — where they differ, the original wins.
Read the original ↗