https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127773

            Bug ID: 127773
           Summary: [x86] vzeroupper missing before call to IPA-RA-known
                    non-AVX callee, causing SSE/AVX transition assists
           Product: gcc
           Version: 13.3.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: target
          Assignee: unassigned at gcc dot gnu.org
          Reporter: michiel.van.slobbe at gmail dot com
  Target Milestone: ---

When an AVX2 function calls a function that GCC compiled without AVX, and
-fipa-ra knows the callee's register usage, GCC can emit the vzeroupper after
the call instead of before it. The callee then runs legacy SSE with dirty upper
YMM state. On Intel P-cores from Golden Cove on, that costs two SSE/AVX-mix
assists per call.

Testcase:

typedef char v32 __attribute__((vector_size(32)));

static __attribute__((noinline)) int f(void) {
  int x;
  __asm__ volatile("pxor %%xmm0, %%xmm0" : "=r"(x) :: "xmm0");  /* legacy SSE
*/
  return x;
}

__attribute__((target("avx2"))) int g(v32 *p) { *p += *p; return f() + 1; }

int main(void) {
  static v32 buf;
  int sink = 0;
  for (int i = 0; i < 100000000; i++) {
    v32 *p = &buf;
    __asm__ volatile("" : "+r"(p));
    sink += g(p);
  }
  return sink == 42;
}

gcc -O2 -S -masm=intel pr.c, trunk (17.0.0 20261007), function g:

        push    rbp
        vmovdqa ymm0, YMMWORD PTR [rdi]
        vpaddb  ymm0, ymm0, ymm0
        mov     rbp, rsp
        and     rsp, -32
        vmovdqa YMMWORD PTR [rdi], ymm0
        call    f
        add     eax, 1
        vzeroupper
        leave
        ret

With -fno-ipa-ra, and with Clang, vzeroupper comes before the call. Same code
with GCC 11 to 16.

gcc -O2 pr.c && perf stat -e task-clock,cpu_core/event=0xc1,umask=0x10/ taskset
-c 0 ./a.out
(100M calls, i7-12700H P-core; 0xc1/0x10 = ASSISTS.SSE_AVX_MIX):

build                 order in g          time       assists
gcc 15.2 -O2          call, vzeroupper    13478 ms   200.0M
gcc 16.0.1 -O2        call, vzeroupper    13370 ms   200.0M
gcc 15.2 -fno-ipa-ra  vzeroupper, call      229 ms   0
clang 22 -O2          vzeroupper, call      243 ms   0

That is about 134 ns vs 2.3 ns per call.

It only happens when every path to the call has dirty upper state. If any path
reaches the call clean, vzeroupper is placed on the dirty edges.

This looks like the check added in r10-3477 (PR 91994 follow-up) in
ix86_avx_u128_mode_needed: if the callee ABI does not clobber all SSE registers
in V4DImode, the call needs AVX_U128_ANY instead of AVX_U128_CLEAN. With
IPA-RA, a non-AVX callee that touches only a few XMM registers takes this path,
even though nothing 256-bit is live across the call. PR 112891 (r14-6447) fixed
the state after such a call, which is why vzeroupper now appears after it, but
not the missing one before it. 

Possible fix: return AVX_U128_CLEAN when no 256/512-bit value is live across
the call, or when the callee is known to be compiled without AVX.

Real-world impact: simdutf's AVX2/AVX-512 kernels call scalar tail routines in
the same TU. With GCC 14 to 16 at -O2/-O3, convert_utf8_to_utf16le on 80 bytes
of ASCII takes 111.8 ns instead of 16.1 ns (2 assists per call); Clang takes
10.0 ns. Workaround with explicit _mm256_zeroupper():
https://github.com/simdutf/simdutf/pull/1049

I did this investigation with Claude (an AI assistant). I reviewed the text and
checked the results.

Reply via email to