AVX, AVX-512, APX and AMX in the calling conventions
Each new register set of x86-64 needs rules for calls. Some come from the psABI and Microsoft, some from the operating system, and one depends on the compiler options of both caller and callee.
The x86-64 ABIs were written for SSE, and each extension since has added registers that calls must deal with: wider vector registers with AVX and AVX-512, mask registers, sixteen more general-purpose registers with APX, and the tiles of AMX. This page collects the rules for them from both 64-bit ABIs, System V and Microsoft x64; the extension pages describe the instructions.
__m256 and __m512 arguments
In System V, a __m256 argument is one SSE eightbyte followed by three
SSEUP ones, and so fills a YMM register; a __m512 fills a ZMM
register. That holds only when the code is compiled for a processor
with those registers. Compiled without AVX, the same function takes
the vector in memory, so caller and callee must be compiled with the
same vector options. GCC and Clang warn about such functions
(-Wpsabi): “AVX vector argument without AVX enabled changes the
ABI”. Variadic calls pass these types on the stack in any case.
-mavx it uses YMM0 and YMM1. Clang for Windows with AVX passes them by reference in RCX and RDX, unless the function is __vectorcall.vec256.c
/* A 32-byte vector, like AVX's __m256. Whether it travels in a YMM
register depends on the processor the code is compiled for. */
typedef float v8sf __attribute__((vector_size(32)));
v8sf add8(v8sf a, v8sf b)
{
return a + b;
}
#ifdef _WIN32
v8sf __vectorcall add8_vc(v8sf a, v8sf b)
{
return a + b;
}
#endif
GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -S vec256.c
add8:
movaps 8(%rsp), %xmm0
addps 40(%rsp), %xmm0
movq %rdi, %rax
movaps %xmm0, (%rdi)
movaps 24(%rsp), %xmm0
addps 56(%rsp), %xmm0
movaps %xmm0, 16(%rdi)
retIntel syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S vec256.c
add8:
movaps xmm0, XMMWORD PTR 8[rsp]
addps xmm0, XMMWORD PTR 40[rsp]
mov rax, rdi
movaps XMMWORD PTR [rdi], xmm0
movaps xmm0, XMMWORD PTR 24[rsp]
addps xmm0, XMMWORD PTR 56[rsp]
movaps XMMWORD PTR 16[rdi], xmm0
retGCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -mavx -S vec256.c
add8:
vaddps %ymm1, %ymm0, %ymm0
retIntel syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -mavx -masm=intel -S vec256.c
add8:
vaddps ymm0, ymm0, ymm1
retClang 23.1.2 x86_64-pc-windows-msvc
AT&T syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -mavx -S vec256.c
add8:
vmovaps (%rcx), %ymm0
vaddps (%rdx), %ymm0, %ymm0
retq
add8_vc@@64:
vaddps %ymm1, %ymm0, %ymm0
retqIntel syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -mavx -masm=intel -S vec256.c
add8:
vmovaps ymm0, ymmword ptr [rcx]
vaddps ymm0, ymm0, ymmword ptr [rdx]
ret
add8_vc@@64:
vaddps ymm0, ymm0, ymm1
ret__m512 values arrive in ZMM0 and ZMM1 and return in ZMM0.vec512.c
/* A 64-byte vector, like AVX-512's __m512, in ZMM registers. */
typedef float v16sf __attribute__((vector_size(64)));
v16sf add16(v16sf a, v16sf b)
{
return a + b;
}
GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -mavx512f -S vec512.c
add16:
vaddps %zmm1, %zmm0, %zmm0
retIntel syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -mavx512f -masm=intel -S vec512.c
add16:
vaddps zmm0, zmm0, zmm1
retClang 23.1.2 x86_64-linux-gnu
AT&T syntax clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -mavx512f -S vec512.c
add16:
vaddps %zmm1, %zmm0, %zmm0
retqIntel syntax clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -mavx512f -masm=intel -S vec512.c
add16:
vaddps zmm0, zmm0, zmm1
retMicrosoft x64 passes every vector wider than 8 bytes by reference,
whatever the processor; only __vectorcall passes __m256 values in
YMM registers. Apple’s documentation for macOS points out the same
dependence in System V: how a vector wider than 16 bytes is passed
depends on the processor features the code is compiled for.
VZEROUPPER
The ABIs do not say who owns the upper halves of the YMM and ZMM
registers across a call: they are scratch. But processors run
instructions of the older SSE encoding slowly while those upper halves
hold data, so compilers clear them with VZEROUPPER before a function
that used them returns or calls another function, which might use SSE.
GCC and Clang do so by default when they compile for AVX; GCC’s manual
gives the reason under its -mvzeroupper option.
square computes in YMM0 and executes VZEROUPPER before it jumps to next.vzeroupper.c
/* After using the upper halves of the YMM registers, a function clears
them with VZEROUPPER before it calls or returns to code that may use
SSE instructions. */
typedef float v8sf __attribute__((vector_size(32)));
extern void next(void);
void square(v8sf *p)
{
*p = *p * *p;
next();
}
GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -mavx -S vzeroupper.c
square:
vmovaps (%rdi), %ymm0
vmulps %ymm0, %ymm0, %ymm0
vmovaps %ymm0, (%rdi)
vzeroupper
jmp next@PLTIntel syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -mavx -masm=intel -S vzeroupper.c
square:
vmovaps ymm0, YMMWORD PTR [rdi]
vmulps ymm0, ymm0, ymm0
vmovaps YMMWORD PTR [rdi], ymm0
vzeroupper
jmp next@PLTClang 23.1.2 x86_64-linux-gnu
AT&T syntax clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -mavx -S vzeroupper.c
square:
vmovaps (%rdi), %ymm0
vmulps %ymm0, %ymm0, %ymm0
vmovaps %ymm0, (%rdi)
vzeroupper
jmp next@PLTIntel syntax clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -mavx -masm=intel -S vzeroupper.c
square:
vmovaps ymm0, ymmword ptr [rdi]
vmulps ymm0, ymm0, ymm0
vmovaps ymmword ptr [rdi], ymm0
vzeroupper
jmp next@PLTAVX-512 mask registers
The eight mask registers K0 to K7 are scratch in System V: a function
may change them, and none carries an argument or a result. A mask
passed to a function is an integer, such as the 16-bit __mmask16,
and travels in a general-purpose register. Microsoft’s convention does
not mention the mask registers; it makes XMM16 to XMM31 scratch.
avx512-mask.c
/* AVX-512 compares produce a mask in a K register, which selects
elements; the mask registers are scratch in every ABI. */
typedef int v16si __attribute__((vector_size(64)));
v16si pick(v16si a, v16si b, v16si c)
{
v16si m = a > b;
return (m & a) | (~m & c);
}
GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -mavx512f -S avx512-mask.c
pick:
vpcmpd $6, %zmm1, %zmm0, %k1
vpblendmd %zmm0, %zmm2, %zmm0{%k1}
retIntel syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -mavx512f -masm=intel -S avx512-mask.c
pick:
vpcmpd k1, zmm0, zmm1, 6
vpblendmd zmm0{k1}, zmm2, zmm0
retClang 23.1.2 x86_64-linux-gnu
AT&T syntax clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -mavx512f -S avx512-mask.c
pick:
vpcmpgtd %zmm1, %zmm0, %k1
vpblendmd %zmm0, %zmm2, %zmm0 {%k1}
retqIntel syntax clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -mavx512f -masm=intel -S avx512-mask.c
pick:
vpcmpgtd k1, zmm0, zmm1
vpblendmd zmm0 {k1}, zmm2, zmm0
retAPX: R16 to R31
APX adds sixteen general-purpose registers. The two 64-bit ABIs treat them differently:
| Registers | System V x86-64 | Microsoft x64 |
|---|---|---|
| R16 to R29 | scratch | scratch |
| R30, R31 | scratch | callee-saved |
Microsoft adds a caveat: jmp_buf has no room for R30 and R31, so a
function must not change them between setjmp and the call that leads
to longjmp. None of the new registers carries arguments, so code
compiled for APX calls code compiled without it, and the other way
round. The APX extension page lists the instructions.
AMX tiles
The eight tile registers TMM0 to TMM7 of AMX are scratch in both ABIs, and so is the tile configuration, TILECFG, in System V: a function that uses tiles configures them with LDTILECFG, and TILERELEASE returns them to their initial state.
Linux adds a step before the first use. The tile data is 8 KB of state
that the kernel does not allocate for every thread, so a process must
ask for it with arch_prctl(ARCH_REQ_XCOMP_PERM, 18), 18 being the
number of the XSAVE component of the tile data. Until it has, the first
AMX instruction raises SIGILL; afterwards, the kernel allocates the
larger buffer for each thread when it first uses the tiles. The
permission holds for the whole process, survives fork and is cleared
by exec. See the AMX tile extension for the
instructions.
Sources
- System V Application Binary Interface, AMD64 Architecture Processor Supplement, version 1.0: commit
e1ce098331da, 2025-03-12 - Microsoft Learn: x64 calling convention: commit
f70d88cd5da7, 2026-09-24 - Microsoft Learn: Overview of x64 ABI conventions: commit
f70d88cd5da7, 2026-09-24 - Microsoft Learn: __vectorcall: commit
f70d88cd5da7, 2026-09-24 - Apple Developer: Writing 64-bit Intel code for Apple Platforms: as published on 2026-09-25
- GCC 14.2 manual: x86 Options: as published on 2026-09-25
- Linux 7.2: Documentation/arch/x86/xstate.rst: commit
8d3ae59288f1, 2026-08-16