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.

Reply via email to