https://gcc.gnu.org/bugzilla/show_bug.cgi?id=128305
Bug ID: 128305
Summary: x86-64: -march=x86-64-v3: unnecessary 32-byte stack
realignment, and DRAP frames, in ordinary code
Product: gcc
Version: 16.2.1
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: target
Assignee: unassigned at gcc dot gnu.org
Reporter: alkondratenko at gmail dot com
Target Milestone: ---
Seen with gcc 12, 13, 14, 15 and 16.2 (Debian 16.2.0-4), with -O2
-march=x86-64-v3 (also -mavx2, -O3). Not seen with clang.
With AVX enabled, gcc often realigns the stack frame to 32 bytes just
to use aligned 256-bit stores to a local struct, in plain non-numeric
code. As far as I know, unaligned 256-bit stores are just as fast on
AVX2 hardware unless they cross a cache line, and for a one-off
zeroing of a local struct that can't pay for realigning the frame on
every call. When such a function also passes an argument on the stack
(e.g. printf with many arguments), the realignment becomes a DRAP
frame, which is more expensive still.
How common this is: gperftools' libtcmalloc_debug.so built at -O2
with -march=x86-64-v3 (in both CFLAGS and CXXFLAGS) has 7 functions
with DRAP frames (2 at -O3 for the C++ part only), and more that
realign without DRAP. clang realigns none of them. In gperftools these
functions aren't hot, but it's easy to imagine hot, frequently called
functions hit by this.
Compared to a normal frame-pointer frame (as used by default on Ubuntu
and Fedora), plain realignment adds one `and`. DRAP adds 5 more
instructions, 2 loads and 2 stores per call, one of which is a copy of
the return address that nothing reads at runtime. DRAP frames also
describe the CFA with a DWARF expression, which many unwinders
(profilers, debuggers) don't handle.
I was under the impression that DRAP is rare in practice; while
developing an unwinder (aw-backtrace) I had trouble getting modern gcc
to emit it at all. Here it shows up in ordinary code.
Test case (tc.c):
--
extern int printf(const char* fmt, ...);
struct stats { long a, b, c, d, e, f, g, h; };
void fill(struct stats *);
long sum(void)
{
#ifndef WORKAROUND
struct stats s;
s.a = 0; s.b = 0; s.c = 0; s.d = 0;
s.e = 0; s.f = 0; s.g = 0; s.h = 0;
#else
struct stats s = {0};
#endif
fill(&s);
// push something on the stack via long arg list
printf("stats: %ld, %ld, %ld, %ld, %ld, %ld\n",
s.a, s.b, s.c, s.d, s.e, s.f);
return s.a + s.h;
}
--
$ gcc -O2 -march=x86-64-v3 -S tc.c
sum:
leaq 8(%rsp), %r10
.cfi_def_cfa 10, 0
andq $-32, %rsp
vpxor %xmm0, %xmm0, %xmm0
pushq -8(%r10)
pushq %rbp
movq %rsp, %rbp
.cfi_escape 0x10,0x6,0x2,0x76,0
pushq %r10
.cfi_escape 0xf,0x3,0x76,0x78,0x6
leaq -80(%rbp), %rdi
subq $72, %rsp
vmovdqa %ymm0, -80(%rbp)
vmovdqa %ymm0, -48(%rbp)
vzeroupper
call fill@PLT
subq $8, %rsp
pushq -40(%rbp)
movq -64(%rbp), %rcx
leaq .LC0(%rip), %rdi
xorl %eax, %eax
movq -72(%rbp), %rdx
movq -48(%rbp), %r9
movq -56(%rbp), %r8
movq -80(%rbp), %rsi
call printf@PLT
movq -8(%rbp), %r10
.cfi_def_cfa 10, 0
movq -24(%rbp), %rax
popq %rdx
addq -80(%rbp), %rax
popq %rcx
leave
leaq -8(%r10), %rsp
.cfi_def_cfa 7, 8
ret
(The epilogue is also missing a .cfi_restore for %rbp, which is
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=128270.)
The two aligned vmovdqa stores are the only reason for the
realignment. Zeroing the struct with {0} instead (-DWORKAROUND) gives
unaligned vmovdqu stores and no realignment. -fno-tree-slp-vectorize
also avoids it.
With -DWORKAROUND there's another oddity: no realignment and no DRAP,
but %rbp is still set up and %r10 is saved for no apparent reason:
$ gcc -O2 -march=x86-64-v3 -DWORKAROUND -S tc.c
sum:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
vpxor %xmm0, %xmm0, %xmm0
movq %rsp, %rbp
.cfi_def_cfa_register 6
pushq %r10
leaq -80(%rbp), %rdi
subq $72, %rsp
.cfi_offset 10, -24
vmovdqu %ymm0, -80(%rbp)
vmovdqu %ymm0, -48(%rbp)
...
DRAP isn't needed for the realignment here: with
-maccumulate-outgoing-args the same function realigns with a plain
frame pointer:
$ gcc -O2 -march=x86-64-v3 -maccumulate-outgoing-args -S tc.c
sum:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
vpxor %xmm0, %xmm0, %xmm0
movq %rsp, %rbp
.cfi_def_cfa_register 6
andq $-32, %rsp
subq $80, %rsp
vmovdqa %ymm0, 16(%rsp)
leaq 16(%rsp), %rdi
vmovdqa %ymm0, 48(%rsp)
vzeroupper
call fill@PLT
...
leave
.cfi_def_cfa 7, 8
ret
All 7 DRAP functions in gperftools likewise use plain realignment with
-maccumulate-outgoing-args. Only some of them follow the exact pattern
of this test case, though: for others, neither {0} nor
-fno-tree-slp-vectorize is relevant, and I haven't narrowed down why
they realign.
So two things would help:
1. Not realigning the frame just to make a few stores to a local
aligned, when unaligned stores would do.
2. When realignment is needed, not using DRAP merely because the
function passes arguments on the stack.