And how hard could it be?
Suppose we have:
struct S {
char buf[9u * 1024];
};
struct S
a(void)
{
return (struct S){0};
}
a() compiles down to:
a:
sub rsp, 8
mov edx, 9216
xor esi, esi
call memset
add rsp, 8
ret
A memset() call is generated by the compiler for zero-initialising. 9K seems to be the threshold at which GCC emits a memset(), instead of rep and friends. Not sure why, given that the latter is (I think) microcoded and vector ops are likely better at any N.
In any case, the above assembly is still interesting. Under the SysV ABI, a() is transformed into:
void
b(struct S *p)
{
memset(p, 0, sizeof(*p));
}
And indeed, if you compile that, you'll get identical* assembly. The caller allocates memory for the return value and passes a pointer to the callee.
In case you're wondering, this is entirely unrelated to RVO from C++.
Now, if you're good at assembly (I'm not) you'll have noticed that what the compiler generated isn't optimal. Compile the manually "transformed" b() and you'd get:
b:
mov edx, 9216
xor esi, esi
jmp memset
which is probably optimal. Ha, the compiler did TCO on the memset() call. How come it couldn't do that with a()? If you check LLVM's IR (at -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
}
The middle-end did manage to tail call memset() in a()! Aside from some attribute differences, both functions lower down to something very similar. So it's really the backend that for some reason fumbles and doesn't generate a tail call.
GCC fails too, I'm not sure why (ABI braindamage?). Though, it doesn't really matter, since no one would write code like that, and the call would be inlined anyway.
Oh, my sweet GCC
While playing around with this, I found a cool GCC bug. I'm a dinosaur and use Debian 13, which ships with GCC 14. If you compile a c() variant with that, you get:
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
Isn't that nice of GCC? It decided to be extra cautious and called memset() twice, just to be sure. And it allocated a struct S from the callee's frame. GCC 15 and onwards are reckless and generate only a single memset() call, and so spit identical assembly to a()'s.
But even with GCC 15, it seems like some edge-cases were missed and you can still get crazy output. If we had this variant:
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
Instead of calling memset(), it creates a 9K object filled with 0s and does memcpy(). Twice. It allocates a struct S r from the callee, copies the 9K object into it, then copies r into the struct S allocated by the caller. Wtf?
The double memcpy() was fixed in GCC 16, but still, a 9K object is copied instead of calling memset():
.LC0:
.zero 2
.zero 9214
d:
sub rsp, 8
mov edx, 9216
mov esi, OFFSET FLAT:.LC0
call memcpy
add rsp, 8
ret
I think this has been fixed in trunk? It's certainly an optimisation bug, not a correctness one. For one more:
struct S
e(void)
{
struct S r = (struct S){0};
return r;
}
The above is like d(), but uses a compound literal to initialise r (like a()). GCC 16 generates a memset() for this, identical to a()'s assembly. And so do earlier versions. Wtf?
If you use something other than 0, it can still trip:
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