Assembly (x86-64, AArch64, RISC-V) Module 19 – The three ISAs today, compared
x86-64 today: levels, APX and AVX10
What x86-64-v2, v3 and v4 promise, how distributions ship them, how Intel APX encodes 32 registers and how AVX10 renumbers AVX-512, with recorded bytes.
What you will learn
- Explain what each x86-64 micro-architecture level promises and how a distribution ships a build for one
- Decode an Intel APX instruction that names r16 to r31 or a new data destination
- Compare how the System V and Microsoft ABIs treat the APX registers
- Distinguish AVX-512 feature flags from an AVX10 version number
Before you start
What you’ll learn · Before you start
What you will learn
- Explain what each x86-64 micro-architecture level promises and how a distribution ships a build for one
- Decode an Intel APX instruction that names r16 to r31 or a new data destination
- Compare how the System V and Microsoft ABIs treat the APX registers
- Distinguish AVX-512 feature flags from an AVX10 version number
Before you start
On this page
Compiled for the x86-64 baseline, a loop that adds two 16-element float arrays takes four 128-bit additions; compiled for x86-64-v3 it takes two, and the v3 build no longer runs on an older processor. Three things decide what current x86-64 code may assume: the micro-architecture levels v2 to v4, Intel’s APX, which takes the general-purpose registers from 16 to 32, and AVX10, which turns AVX-512’s many feature flags into one version number.
This page reads each of them in recorded compiler and assembler output from LLVM 23 and NASM 3.02. Every listing is compiled or assembled for x86-64 and read as the compiler’s assembly or as the encoded bytes; running APX or AVX10 code needs a processor that implements them, so the page shows what the listings say rather than what a run printed.
A level is a promise about a whole set of features
The x86-64 psABI groups processor features into named micro-architecture level: A named set of x86-64 processor features defined by the psABI: x86-64-v2, v3 and v4. Code built for a level may use all of its features, and those of the levels below it, without testing for them at run time.. A build for a level may use every feature of that level and of the levels below it, with no test at run time. The table is the psABI’s, cut to the features that change everyday code:
| Level | What it adds | One instruction it allows |
|---|---|---|
| baseline | SSE, SSE2, CMOV, CMPXCHG8B, SYSCALL | cvtss2si |
| x86-64-v2 | SSE3 to SSE4.2, SSSE3, POPCNT, CMPXCHG16B, LAHF-SAHF | popcnt |
| x86-64-v3 | AVX, AVX2, BMI1, BMI2, FMA, F16C, LZCNT, MOVBE | vfmadd132pd |
| x86-64-v4 | AVX-512 F, BW, CD, DQ and VL | kmovw |
A compiler reads the level as a list of switches. The first half of the session prints the feature macros clang defines
for each -march level, and the second half compiles three small functions from level.c for the baseline and for v3:
/* Three small functions whose best instructions depend on the x86-64 level the compiler may assume. */
unsigned shift_left(unsigned x, unsigned n) { return x << (n & 31); }
float scale_add(float a, float b, float c) { return a * b + c; }
void add_arrays(float *restrict out, const float *restrict a, const float *restrict b) {
for (int i = 0; i < 16; i++) out[i] = a[i] + b[i];
} import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session
FEATURES = "'__(SSE2|SSE3|SSSE3|SSE4_1|SSE4_2|POPCNT|AVX|AVX2|BMI|BMI2|F16C|FMA|LZCNT|MOVBE|AVX512(F|BW|CD|DQ|VL))__'"
session.run(f"""
# The feature macros clang defines for each level: what code built for it may use without asking
clang-23 --target=x86_64-linux-gnu -march=x86-64 -dM -E -x c /dev/null | grep -oE {FEATURES} | sort | tr '\\n' ' '
clang-23 --target=x86_64-linux-gnu -march=x86-64-v2 -dM -E -x c /dev/null | grep -oE {FEATURES} | sort | tr '\\n' ' '
clang-23 --target=x86_64-linux-gnu -march=x86-64-v3 -dM -E -x c /dev/null | grep -oE {FEATURES} | sort | tr '\\n' ' '
clang-23 --target=x86_64-linux-gnu -march=x86-64-v4 -dM -E -x c /dev/null | grep -oE {FEATURES} | sort | tr '\\n' ' '
""")
session.run("""
# The same three functions for the baseline and for x86-64-v3: the shift, the multiply-add and the loop's width
clang-23 --target=x86_64-linux-gnu -O2 -march=x86-64 -masm=intel -S -o - level.c | grep -E '^\\s+(shl|mulss|addss|addps)'
clang-23 --target=x86_64-linux-gnu -O2 -march=x86-64-v3 -masm=intel -S -o - level.c | grep -E '^\\s+(shlx|vfmadd|vaddps)'
# x86-64-v4 allows 512-bit vectors, but the loop stays at 256 bits until the build asks for 512
clang-23 --target=x86_64-linux-gnu -O2 -march=x86-64-v4 -masm=intel -S -o - level.c | grep -E '^\\s+vaddps'
clang-23 --target=x86_64-linux-gnu -O2 -march=x86-64-v4 -mprefer-vector-width=512 -masm=intel -S -o - level.c | grep -E '^\\s+vaddps'
""") Output
# The feature macros clang defines for each level: what code built for it may use without asking $ clang-23 --target=x86_64-linux-gnu -march=x86-64 -dM -E -x c /dev/null | grep -oE '__(SSE2|SSE3|SSSE3|SSE4_1|SSE4_2|POPCNT|AVX|AVX2|BMI|BMI2|F16C|FMA|LZCNT|MOVBE|AVX512(F|BW|CD|DQ|VL))__' | sort | tr '\n' ' ' __SSE2__ $ clang-23 --target=x86_64-linux-gnu -march=x86-64-v2 -dM -E -x c /dev/null | grep -oE '__(SSE2|SSE3|SSSE3|SSE4_1|SSE4_2|POPCNT|AVX|AVX2|BMI|BMI2|F16C|FMA|LZCNT|MOVBE|AVX512(F|BW|CD|DQ|VL))__' | sort | tr '\n' ' ' __POPCNT__ __SSE2__ __SSE3__ __SSE4_1__ __SSE4_2__ __SSSE3__ $ clang-23 --target=x86_64-linux-gnu -march=x86-64-v3 -dM -E -x c /dev/null | grep -oE '__(SSE2|SSE3|SSSE3|SSE4_1|SSE4_2|POPCNT|AVX|AVX2|BMI|BMI2|F16C|FMA|LZCNT|MOVBE|AVX512(F|BW|CD|DQ|VL))__' | sort | tr '\n' ' ' __AVX__ __AVX2__ __BMI__ __BMI2__ __F16C__ __FMA__ __LZCNT__ __MOVBE__ __POPCNT__ __SSE2__ __SSE3__ __SSE4_1__ __SSE4_2__ __SSSE3__ $ clang-23 --target=x86_64-linux-gnu -march=x86-64-v4 -dM -E -x c /dev/null | grep -oE '__(SSE2|SSE3|SSSE3|SSE4_1|SSE4_2|POPCNT|AVX|AVX2|BMI|BMI2|F16C|FMA|LZCNT|MOVBE|AVX512(F|BW|CD|DQ|VL))__' | sort | tr '\n' ' ' __AVX__ __AVX2__ __AVX512BW__ __AVX512CD__ __AVX512DQ__ __AVX512F__ __AVX512VL__ __BMI__ __BMI2__ __F16C__ __FMA__ __LZCNT__ __MOVBE__ __POPCNT__ __SSE2__ __SSE3__ __SSE4_1__ __SSE4_2__ __SSSE3__ # The same three functions for the baseline and for x86-64-v3: the shift, the multiply-add and the loop's width $ clang-23 --target=x86_64-linux-gnu -O2 -march=x86-64 -masm=intel -S -o - level.c | grep -E '^\s+(shl|mulss|addss|addps)' shl eax, cl mulss xmm0, xmm1 addss xmm0, xmm2 addps xmm1, xmm0 addps xmm1, xmm0 addps xmm1, xmm0 addps xmm1, xmm0 $ clang-23 --target=x86_64-linux-gnu -O2 -march=x86-64-v3 -masm=intel -S -o - level.c | grep -E '^\s+(shlx|vfmadd|vaddps)' shlx eax, edi, esi vfmadd213ss xmm0, xmm1, xmm2 # xmm0 = (xmm1 * xmm0) + xmm2 vaddps ymm0, ymm0, ymmword ptr [rdx] vaddps ymm0, ymm0, ymmword ptr [rdx + 32] # x86-64-v4 allows 512-bit vectors, but the loop stays at 256 bits until the build asks for 512 $ clang-23 --target=x86_64-linux-gnu -O2 -march=x86-64-v4 -masm=intel -S -o - level.c | grep -E '^\s+vaddps' vaddps ymm0, ymm0, ymmword ptr [rdx] vaddps ymm0, ymm0, ymmword ptr [rdx + 32] $ clang-23 --target=x86_64-linux-gnu -O2 -march=x86-64-v4 -mprefer-vector-width=512 -masm=intel -S -o - level.c | grep -E '^\s+vaddps' vaddps zmm0, zmm0, zmmword ptr [rdx]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 levels_session.py
Each level’s line is a superset of the one before: the baseline defines only __SSE2__ of the macros asked for, v2
adds five, v3 adds eight more and v4 adds the five AVX-512 subsets. The code follows. The variable shift becomes one
shlx instead of a move into cl and a shl, the multiply and add become one vfmadd213ss, and the loop’s four
128-bit addps become two 256-bit vaddps.
The v4 lines carry a detail worth remembering. The level allows 512-bit vectors, but clang kept the loop at 256 bits
until the build added -mprefer-vector-width=512; only then did it emit one vaddps on zmm0. A level states what
the processor has, while the compiler’s tuning still decides what is worth using.
Key idea
A level is a contract for a whole set of features at once: build for x86-64-v3 and the code may use AVX2, FMA and BMI2 everywhere, but it then needs a v3 processor to run at all.
Distributions ship the baseline and a faster copy
A v3 build cannot run on an older processor: the psABI warns that loading such a library fails there, and code that does start stops with SIGILL at the first instruction the processor lacks. So a distribution cannot simply switch its whole archive to v3. The psABI describes the usual way out: install the optimised shared object in a glibc-hwcaps folder: A subfolder of a library folder named after a hardware level, such as glibc-hwcaps/x86-64-v3. Since glibc 2.33 the dynamic linker searches the folders of the levels the processor supports, best first, before the default folder. folder named after its level, and keep a baseline build in the default folder. Since glibc 2.33 the dynamic linker searches those folders on its own, best level first.
Step through one load on a processor that reaches x86-64-v3 but has no AVX-512:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A library is needed. The program lists libdemo.so.1 among the libraries it needs. The package installed two builds of it, one built for the baseline in the default folder and one built with -march=x86-64-v3.
- The processor sets the list. This processor has every x86-64-v3 feature but not AVX512F, so the dynamic linker searches the v3 folder, then the v2 folder, then the default folder, in that order.
- The v4 folder is left out. x86-64-v4 needs AVX-512, which this processor lacks, so its folder is not on the list at all. A v4 build placed there could never be loaded by mistake on this machine.
- The v3 copy is found. The v3 folder holds libdemo.so.1, so the loader maps that copy. Its code may use AVX2, FMA and BMI2 without testing for them first, because the level promised they exist.
- The rest is not read. Once a copy is found the search stops, so the v2 folder and the default folder are not read for this library. A machine without x86-64-v3 would load the default copy instead.
- Why the baseline copy stays. The psABI asks for the fallback in the default folder to be built for the baseline, so the same package still loads on a processor that reaches none of the levels.
The search list is computed from the processor, so the v4 folder never appears on it and the v3 copy wins. The same package loads its baseline copy on a machine that stops at v2, and nobody has to detect anything.
Distributions use the levels in three ways. Some require a level for the whole system, which ends support for older
processors. Some ship optimised copies of a few libraries in glibc-hwcaps folders, as above. Ubuntu added a third in
25.10: an architecture variant, a second build of whole packages. Its 26.04 LTS release notes describe amd64v3, a
variant optimised for x86-64-v3 that is opt-in by default; the notes switch it on with one APT setting,
APT::Architecture-Variants. The same notes say Ubuntu’s 26.04 cloud images are built with AMD64v3 by default.
Copying a v3 library into the default folder
Replacing the baseline copy with the v3 build makes the package fail on every processor below v3, at load time or
at the first AVX2 instruction. Put the v3 build in glibc-hwcaps/x86-64-v3 beside the baseline copy, which the
psABI asks to stay built with -march=x86-64.
Checkpoint
Check: What a micro-architecture level promises · How distributions ship level builds
APX adds sixteen registers and a destination operand
Intel APX, the Advanced Performance Extensions, adds the extended general-purpose registers: The sixteen general-purpose registers r16 to r31 that Intel APX adds to x86-64's rax to r15, reached through the REX2 prefix or an extended EVEX prefix. r16 to r31, three-operand forms with a new data destination: An Intel APX form in which an instruction such as add writes its result to a third register and only reads its two sources, as in add r20, r21, r22.,
forms that leave the flags untouched, push2 and pop2, and conditional compares. Two prefixes carry it. An existing
one- or two-byte-opcode instruction reaches r16 to r31 through the two-byte
REX2 prefix: A two-byte x86-64 prefix defined by Intel APX: the byte 0xD5 and a payload byte whose bits extend register numbers to five bits, so one- and two-byte-opcode instructions can name r16 to r31., and what REX2 cannot express, such as a new data destination, goes through an
extended EVEX prefix, the four-byte prefix AVX-512 introduced.
NASM 3.00 added APX, so NASM 3.02 assembles apx.asm, and LLVM’s disassembler reads each instruction back:
; Intel APX forms for NASM 3.02 in a 64-bit ELF object: the lesson reads their bytes.
bits 64
section .text
global apx_forms
apx_forms:
add r20, r21 ; an old two-operand add on extended registers
add r20, r21, r22 ; new data destination: r20 = r21 + r22
{nf} add rax, rbx ; the same add without writing the flags
push2 r16, r17 ; two registers saved by one instruction
pop2 r17, r16 ; and restored in the opposite order
mov r31, [r30 + 8] ; a load with extended base and destination
ret import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session
session.run("""
# NASM 3.02 assembles the APX forms; LLVM's disassembler reads every byte back (AT&T order: destination last)
nasm -f elf64 apx.asm -o apx.o
llvm-objdump -d --no-addresses apx.o | grep -E '^\\s+[0-9a-f]{2} '
# LLVM's assembler picks a different, equally valid encoding for the three-operand add
echo 'add r20, r21, r22' | llvm-mc -triple=x86_64 -x86-asm-syntax=intel -output-asm-variant=1 -show-encoding
echo 'add r20, r21, r22' | llvm-mc -triple=x86_64 -x86-asm-syntax=intel -filetype=obj -o add.o
llvm-objdump -d --no-addresses add.o | grep -E '^\\s+[0-9a-f]{2} '
""") Output
# NASM 3.02 assembles the APX forms; LLVM's disassembler reads every byte back (AT&T order: destination last)
$ nasm -f elf64 apx.asm -o apx.o
$ llvm-objdump -d --no-addresses apx.o | grep -E '^\s+[0-9a-f]{2} '
d5 58 01 ec addq %r21, %r20
62 ec dc 10 03 ee addq %r22, %r21, %r20
62 f4 fc 0c 03 c3 {nf} addq %rbx, %rax
62 fc 7c 10 ff f1 push2 %r17, %r16
62 fc 74 10 8f c0 pop2 %r16, %r17
d5 5d 8b 7e 08 movq 0x8(%r30), %r31
c3 retq
# LLVM's assembler picks a different, equally valid encoding for the three-operand add
$ echo 'add r20, r21, r22' | llvm-mc -triple=x86_64 -x86-asm-syntax=intel -output-asm-variant=1 -show-encoding
add r20, r21, r22 # encoding: [0x62,0xec,0xdc,0x10,0x01,0xf5]
$ echo 'add r20, r21, r22' | llvm-mc -triple=x86_64 -x86-asm-syntax=intel -filetype=obj -o add.o
$ llvm-objdump -d --no-addresses add.o | grep -E '^\s+[0-9a-f]{2} '
62 ec dc 10 01 f5 addq %r22, %r21, %r20
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 apx_bytes_session.py
The two-operand add r20, r21 is four bytes, d5 58 01 ec: REX2, then the ordinary ADD opcode and its ModRM byte.
The three-operand add, the no-flags add, push2 and pop2 start with 62, the EVEX prefix, and take six bytes; the
load from [r30 + 8] needs only REX2 again, five bytes with its displacement. Step through the four bytes to see where
the fifth register bit of each operand hides:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The prefix byte. The byte 0xD5 starts a REX2 prefix. The APX specification makes REX2 two bytes long, this byte and one payload byte, and it is valid only in 64-bit mode with APX enabled.
- The payload bits. 0x58 is 0101 1000, read as M0, R4, X4, B4, W, R3, X3 and B3. M0 = 0 picks the one-byte opcode map, W = 1 makes the add 64 bits wide, and R4 = 1 and B4 = 1 are fifth register bits.
- The opcode. 0x01 is the ordinary ADD that adds a register into a register or memory operand. REX2 changes which registers it can name, not what the opcode does.
- The ModRM byte. 0xEC is 11 101 100. Mod 11 says both operands are registers; reg = 101 and r/m = 100 are the low three bits of the source and destination register numbers.
- Join the bits. The source in ModRM.reg joins R4, R3 and its three bits, 1, 0, 101, so it is register 21. The destination in ModRM.r/m joins B4, B3 and its three bits, 1, 0, 100, so it is register 20. The bytes say add r20, r21.
- What the extra byte buys. The same add of two original registers needs a one-byte REX prefix; REX2 costs one more byte and lets either operand be any of 32 registers. Three-operand forms need the six-byte EVEX encoding instead.
The last lines of the session hold a surprise worth keeping. NASM encoded add r20, r21, r22 with opcode 03, LLVM
with 01 and a different ModRM byte, and both disassemble to the same instruction. ADD has two opcodes, 01 and 03,
that differ only in which ModRM field names which operand. With a new destination both name two sources and add them,
so two correct assemblers may give different bytes. Compare encodings by their disassembly, never byte for byte.
Linux and Windows split r16 to r31 differently
Sixteen more registers help only if the ABI lets a function use them cheaply. The two x86-64 ABIs disagree:
| Registers | System V (Linux and other Unix-like systems) | Microsoft x64 (Windows) |
|---|---|---|
| r16 to r29 | caller-saved: any call may change them | volatile: any call may change them |
| r30 and r31 | caller-saved | nonvolatile: a function that writes them saves them first |
The psABI keeps only rbp, rbx and r12 to r15 for the caller, so r16 to r31 belong to the called function, and a Linux
function may write all of them without saving anything. Microsoft keeps r30 and r31 callee-saved register: A register whose value a called function must hand back unchanged: if it writes the register, it saves the old value first and restores it before it returns. rbx on x86-64, x19 on AArch64 and s0 on RISC-V are callee-saved. More in “Callee-saved registers and stack frames”, and
adds a rule for code between setjmp and longjmp: r30 and r31 are not part of jmp_buf, so such a function must
leave them unchanged.
The second session compiles mix28.c, a function with 28 loaded values that all stay live, for both ABIs, with and
without -mapxf:
/* Twenty-eight loaded values that all stay live to the last line: more than sixteen registers can hold. */
long mix28(const long *p) {
long v0 = p[0], v1 = p[1], v2 = p[2], v3 = p[3], v4 = p[4], v5 = p[5], v6 = p[6];
long v7 = p[7], v8 = p[8], v9 = p[9], v10 = p[10], v11 = p[11], v12 = p[12], v13 = p[13];
long v14 = p[14], v15 = p[15], v16 = p[16], v17 = p[17], v18 = p[18], v19 = p[19], v20 = p[20];
long v21 = p[21], v22 = p[22], v23 = p[23], v24 = p[24], v25 = p[25], v26 = p[26], v27 = p[27];
long s = v0 * v1 + v1 * v2 + v2 * v3 + v3 * v4 + v4 * v5 + v5 * v6 + v6 * v7
+ v7 * v8 + v8 * v9 + v9 * v10 + v10 * v11 + v11 * v12 + v12 * v13 + v13 * v14
+ v14 * v15 + v15 * v16 + v16 * v17 + v17 * v18 + v18 * v19 + v19 * v20 + v20 * v21
+ v21 * v22 + v22 * v23 + v23 * v24 + v24 * v25 + v25 * v26 + v26 * v27 + v27 * v0;
long x = v0 ^ v1 ^ v2 ^ v3 ^ v4 ^ v5 ^ v6 ^ v7 ^ v8 ^ v9 ^ v10 ^ v11 ^ v12 ^ v13
^ v14 ^ v15 ^ v16 ^ v17 ^ v18 ^ v19 ^ v20 ^ v21 ^ v22 ^ v23 ^ v24 ^ v25 ^ v26 ^ v27;
return s ^ x;
} import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session
session.run("""
# 28 live values: the instructions, then the spill and reload slots, without APX and with it (System V, Linux)
clang-23 --target=x86_64-linux-gnu -O2 -S -o - mix28.c | grep -cE '^\\s+[a-z{]'
clang-23 --target=x86_64-linux-gnu -O2 -S -o - mix28.c | grep -cE 'Spill|Reload'
clang-23 --target=x86_64-linux-gnu -O2 -mapxf -S -o - mix28.c | grep -cE '^\\s+[a-z{]'
clang-23 --target=x86_64-linux-gnu -O2 -mapxf -S -o - mix28.c | grep -cE 'Spill|Reload'
# The registers each ABI makes the function save: Linux keeps r16-r31 free, Windows saves r30 and r31
clang-23 --target=x86_64-linux-gnu -O2 -mapxf -masm=intel -S -o - mix28.c | grep -E '^\\s+push'
clang-23 --target=x86_64-pc-windows-msvc -O2 -mapxf -masm=intel -S -o - mix28.c | grep -E '^\\s+push'
""") Output
# 28 live values: the instructions, then the spill and reload slots, without APX and with it (System V, Linux)
$ clang-23 --target=x86_64-linux-gnu -O2 -S -o - mix28.c | grep -cE '^\s+[a-z{]'
133
$ clang-23 --target=x86_64-linux-gnu -O2 -S -o - mix28.c | grep -cE 'Spill|Reload'
38
$ clang-23 --target=x86_64-linux-gnu -O2 -mapxf -S -o - mix28.c | grep -cE '^\s+[a-z{]'
117
$ clang-23 --target=x86_64-linux-gnu -O2 -mapxf -S -o - mix28.c | grep -cE 'Spill|Reload'
6
# The registers each ABI makes the function save: Linux keeps r16-r31 free, Windows saves r30 and r31
$ clang-23 --target=x86_64-linux-gnu -O2 -mapxf -masm=intel -S -o - mix28.c | grep -E '^\s+push'
pushp rbp
push2p r15, r14
push2p r13, r12
pushp rbx
$ clang-23 --target=x86_64-pc-windows-msvc -O2 -mapxf -masm=intel -S -o - mix28.c | grep -E '^\s+push'
pushp r31
push2p r30, r15
push2p r14, r13
push2p r12, rsi
push2p rdi, rbp
pushp rbx
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 apx_abi_session.py
With 16 registers the Linux build has 133 instructions and 38 spills and reloads, the stack stores and loads the
compiler marks when values do not fit in registers. With APX it has 117 instructions and 6 of them, and it uses r16 to
r31 without saving any: its prologue saves only the six classic callee-saved registers, in four pushp and push2p
instructions, where the final p is the push-pop hint the APX specification defines for paired saves and restores. The Windows build saves r31 and r30 as well, together with rsi and rdi, which Microsoft
also keeps callee-saved, because the function used them.
Key idea
APX doubles the general-purpose registers, and the ABI decides what they cost: on Linux a function may write all sixteen without saving them, on Windows r30 and r31 must be saved and restored like rbx.
Checkpoint
Check: APX registers and encodings · The APX registers in the two ABIs
AVX10 turns AVX-512 into one numbered feature
AVX-512 brought 512-bit registers, 32 vector registers and eight mask registers, then grew one CPUID flag per family of instructions until code had to test a long list before using any of them. Intel’s answer is AVX10: Intel's converged vector instruction set, based on AVX-512 and including all of its instructions, enumerated by one CPUID flag and an inclusive version number instead of one flag per instruction family.. The SDM’s chapter on it states that AVX10 is based on AVX-512 and includes all of its instructions, and that support is read as one CPUID flag in leaf 7 plus a version number in leaf 24H. Versions are inclusive, so version N+1 has everything in version N.
Walk through the vector extensions in order:
- SSE2
The 128-bit floor of every x86-64 processor baseline
SSE2 is part of the psABI baseline, so any x86-64 program may use the 128-bit xmm registers without testing for them.
- SSE4.2, POPCNT
More 128-bit operations and a bit count x86-64-v2
SSE3, SSSE3, SSE4.1, SSE4.2 and POPCNT join CMPXCHG16B and LAHF-SAHF in x86-64-v2, still with 128-bit registers.
- AVX, AVX2
256-bit ymm registers through the VEX prefix x86-64-v3
AVX widened the vector registers to 256 bits with the VEX prefix and AVX2 brought integer work to that width. With FMA, BMI1, BMI2, F16C, LZCNT and MOVBE they make up x86-64-v3.
- AVX-512
512 bits, 32 vector registers and 8 masks x86-64-v4
AVX-512 added zmm registers, vector registers 16 to 31 and the opmask registers k0 to k7, and vector instructions reach all three through the EVEX prefix. Its F, CD, BW, DQ and VL subsets make up x86-64-v4.
- More AVX-512
One CPUID flag for every new family
Later processors added VBMI, IFMA, VNNI, BF16, VPOPCNTDQ, VBMI2, BITALG and FP16, each with a feature flag of its own, so code tested every family it used.
- AVX10.1
The AVX-512 set under one version number new enumeration
AVX10 version 1 gathers those families into one feature with a version number, read from CPUID leaf 24H, so a program checks for AVX10 and a version at least N instead of a list of flags.
- AVX10.2
New instructions, every vector width new instructions
AVX10.2 adds instructions such as vminmaxps, and every processor that reports AVX10 supports all three vector widths. Intel adds new vector features only to AVX10 from here on.
The timeline has 7 events, from SSE2 to AVX10.2.
The encodings show why EVEX is the prefix that matters. The same vaddps ymm0, ymm1, ymm2 is four bytes in VEX and six
when forced into EVEX; registers 16 to 31, masking with k1 and AVX10.2’s vminmaxps exist only in EVEX:
; Vector encodings for NASM 3.02 in a 64-bit ELF object: the lesson reads their bytes.
bits 64
section .text
global vector_forms
vector_forms:
vaddps ymm0, ymm1, ymm2 ; AVX: a VEX prefix
{evex} vaddps ymm0, ymm1, ymm2 ; the same operation in EVEX
vaddps ymm16, ymm17, ymm18 ; registers 16-31 exist only in EVEX
vaddps zmm0{k1}{z}, zmm1, zmm2 ; 512 bits, mask k1, zeroing
vminmaxps zmm0, zmm1, zmm2, 0 ; an AVX10.2 instruction
ret import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session
FEATURES = "'__(AVX512(F|BW|CD|DQ|VL)|AVX10_[12](_512)?)__'"
session.run("""
# One vaddps in two encodings, the registers only EVEX can name, a mask, and an AVX10.2 instruction
nasm -f elf64 vec.asm -o vec.o
llvm-objdump -d -M intel --no-addresses vec.o | grep -E '^\\s+[0-9a-f]{2} '
""")
session.run(f"""
# What clang may assume for x86-64-v4, for AVX10.1 and for AVX10.2
clang-23 --target=x86_64-linux-gnu -march=x86-64-v4 -dM -E -x c /dev/null | grep -oE {FEATURES} | sort | tr '\\n' ' '
clang-23 --target=x86_64-linux-gnu -mavx10.1 -dM -E -x c /dev/null | grep -oE {FEATURES} | sort | tr '\\n' ' '
clang-23 --target=x86_64-linux-gnu -mavx10.2 -dM -E -x c /dev/null | grep -oE {FEATURES} | sort | tr '\\n' ' '
""") Output
# One vaddps in two encodings, the registers only EVEX can name, a mask, and an AVX10.2 instruction
$ nasm -f elf64 vec.asm -o vec.o
$ llvm-objdump -d -M intel --no-addresses vec.o | grep -E '^\s+[0-9a-f]{2} '
c5 f4 58 c2 vaddps ymm0, ymm1, ymm2
62 f1 74 28 58 c2 vaddps ymm0, ymm1, ymm2
62 a1 74 20 58 c2 vaddps ymm16, ymm17, ymm18
62 f1 74 c9 58 c2 vaddps zmm0 {k1} {z}, zmm1, zmm2
62 f3 75 48 52 c2 00 vminmaxps zmm0, zmm1, zmm2, 0x0
c3 ret
# What clang may assume for x86-64-v4, for AVX10.1 and for AVX10.2
$ clang-23 --target=x86_64-linux-gnu -march=x86-64-v4 -dM -E -x c /dev/null | grep -oE '__(AVX512(F|BW|CD|DQ|VL)|AVX10_[12](_512)?)__' | sort | tr '\n' ' '
__AVX512BW__ __AVX512CD__ __AVX512DQ__ __AVX512F__ __AVX512VL__
$ clang-23 --target=x86_64-linux-gnu -mavx10.1 -dM -E -x c /dev/null | grep -oE '__(AVX512(F|BW|CD|DQ|VL)|AVX10_[12](_512)?)__' | sort | tr '\n' ' '
__AVX10_1__ __AVX10_1_512__ __AVX512BW__ __AVX512CD__ __AVX512DQ__ __AVX512F__ __AVX512VL__
$ clang-23 --target=x86_64-linux-gnu -mavx10.2 -dM -E -x c /dev/null | grep -oE '__(AVX512(F|BW|CD|DQ|VL)|AVX10_[12](_512)?)__' | sort | tr '\n' ' '
__AVX10_1__ __AVX10_1_512__ __AVX10_2__ __AVX10_2_512__ __AVX512BW__ __AVX512CD__ __AVX512DQ__ __AVX512F__ __AVX512VL__
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 vectors_session.py
The macro lines show the overlap in a compiler’s terms: of the macros the session asks for, -mavx10.1 turns on the
same five AVX-512 subsets as x86-64-v4 and adds __AVX10_1__, and -mavx10.2 adds __AVX10_2__ on top. One change sits in the AVX10 technical paper’s own
history. Its earlier revisions referred to a 256-bit maximum vector size and to enumerating vector lengths; revision 3.0
removed both, and the SDM now says every processor that reports AVX10 supports all three widths.
New vector features arrive only in AVX10
The SDM says new vector instructions will be added only to AVX10 from now on, so a program that needs a recent one tests for AVX10 and a version, not for another AVX-512 flag.
Checkpoint
Check: From AVX-512 flags to AVX10 versions
Practise
Each task changes a copy of one of this page’s files and re-runs its session:
- Add
unsigned count(unsigned long x) { return __builtin_popcountl(x); }tolevel.cand compile it for the baseline and forx86-64-v2. Predict which build gets onepopcnt, then check. - In
apx.asm, changeadd r20, r21toadd rax, rbxand compare the lengths: the REX prefix makes three bytes, REX2 four. - Compile
mix28.cwith-mapxfforx86_64-pc-windows-msvcagain, cut it to 20 values, and see whetherr30andr31still appear in the prologue.
The questions below ask which level, prefix or extension each instruction needs:
Practice that adapts to you
Practise this lesson
16 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
What is x86-64-v3, and what does building a library with -march=x86-64-v3 promise?
x86-64-v3 is the x86-64 psABI micro-architecture level that adds AVX, AVX2, BMI1, BMI2, FMA, F16C, LZCNT and MOVBE
to the baseline and to v2. Building with -march=x86-64-v3 lets the compiler use all of them anywhere, with no test at
run time, so the library is faster on a processor that has them and fails with SIGILL on one that does not. That is why
such a build goes into a glibc-hwcaps/x86-64-v3 folder beside a baseline copy, and the dynamic linker picks it only on
a processor that reaches the level.
Two harder questions sit in this module’s bank. One starts from a crash on an older server that no machine in your CI can reproduce; the other keeps values in the APX registers across a call into code built without APX.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Your Linux library ships a baseline copy and a glibc-hwcaps/x86-64-v3 copy. A customer reports that it crashes on an older server, and you cannot reproduce it, because every machine in your CI reaches x86-64-v3. How would you tell which copy that server loads, and how would you test the copy your CI never runs?
-
Senior to staff
A hand-written APX routine for Linux keeps eight running totals in r24 to r31 while it calls a comparison callback that customers compile themselves. Every test succeeds, and every test callback was built without APX. Is the routine correct, and how would you write its register use for Linux and for Windows?
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What is an x86-64 micro-architecture level, and what does building for x86-64-v3 promise?
A named set of features defined by the psABI: v2, v3 and v4. Code built with -march=x86-64-v3 may use AVX, AVX2, BMI1, BMI2, FMA, F16C, LZCNT and MOVBE anywhere without a run-time test, so it is faster on a v3 processor and stops with SIGILL on an older one.
-
How do Linux distributions ship code for newer x86-64 levels without dropping older processors?
They keep the baseline build and add level-specific copies in glibc-hwcaps folders, such as glibc-hwcaps/x86-64-v3, which the dynamic linker picks only on a processor that reaches the level. Some ship a whole package variant, such as Ubuntu's amd64v3.
-
What does Intel APX add to the general-purpose registers, and how are the new ones reached?
Sixteen more registers, r16 to r31, reached through the two-byte REX2 prefix (0xD5 and a payload byte) or an extended EVEX prefix. Two correct assemblers may pick different bytes for the same APX instruction.
-
What is APX's new data destination form?
An instruction such as add writes its result to a third register and only reads its two sources, as in add r20, r21, r22, instead of overwriting one of them.
-
How do Linux and Windows treat the APX registers r16 to r31 across calls?
System V lets a function write all sixteen without saving them. Microsoft's ABI makes r30 and r31 callee-saved, so a Windows function saves and restores them like rbx.
-
What does AVX10 change about how x86-64 vector features are named?
It includes all of AVX-512 behind one CPUID feature flag and an inclusive version number, instead of one flag per instruction family, and new vector instructions now arrive only there.
Key takeaways
- An x86-64 micro-architecture level names a whole set of features; code built for x86-64-v3 uses AVX2, FMA and BMI2 freely and needs a v3 processor to run.
- Distributions keep a baseline build and add level-specific copies in glibc-hwcaps folders, or a whole package variant such as Ubuntu’s amd64v3, so older processors keep working.
- APX reaches r16 to r31 through the two-byte REX2 prefix or an extended EVEX prefix, and two correct assemblers may pick different bytes for the same APX instruction.
- System V lets a function write all sixteen new registers without saving them, while Microsoft’s ABI makes r30 and r31 callee-saved.
- AVX10 includes all of AVX-512 behind one feature flag and an inclusive version number, and new vector instructions now arrive only there.
Terms in this lesson
- AVX10
- Intel's converged vector instruction set, based on AVX-512 and including all of its instructions, enumerated by one CPUID flag and an inclusive version number instead of one flag per instruction family.
- extended general-purpose registers
- The sixteen general-purpose registers r16 to r31 that Intel APX adds to x86-64's rax to r15, reached through the REX2 prefix or an extended EVEX prefix.
- glibc-hwcaps folder
- A subfolder of a library folder named after a hardware level, such as glibc-hwcaps/x86-64-v3. Since glibc 2.33 the dynamic linker searches the folders of the levels the processor supports, best first, before the default folder.
- micro-architecture level
- A named set of x86-64 processor features defined by the psABI: x86-64-v2, v3 and v4. Code built for a level may use all of its features, and those of the levels below it, without testing for them at run time.
- new data destination
- An Intel APX form in which an instruction such as add writes its result to a third register and only reads its two sources, as in add r20, r21, r22.
- REX2 prefix
- A two-byte x86-64 prefix defined by Intel APX: the byte 0xD5 and a payload byte whose bits extend register numbers to five bits, so one- and two-byte-opcode instructions can name r16 to r31.
References
- System V Application Binary Interface, AMD64 Architecture Processor Supplement (micro-architecture levels) (x86-64 psABI project)
- Intel Advanced Performance Extensions (Intel APX) Architecture Specification, revision 9.0 (Intel Corporation)
- Intel 64 and IA-32 Architectures Software Developer Manuals (chapter 16, Programming with Intel AVX10) (Intel Corporation)
- The Converged Vector ISA: Intel Advanced Vector Extensions 10, technical paper revision 3.0 (Intel Corporation)
- Intel Advanced Vector Extensions 10.2 Architecture Specification, revision 7.0 (Intel Corporation)
- x64 ABI conventions: register volatility and preservation (Microsoft)
- x64 calling convention (APX registers and setjmp) (Microsoft)
- ld.so(8): glibc hardware capabilities (Linux man-pages project)
- Ubuntu 26.04 LTS release notes: summary for LTS users (architecture variants and amd64v3) (Canonical)
- NASM version history (APX and AVX10 support in NASM 3.00) (The NASM development team)
- Clang command line argument reference (-mapxf, -mavx10.2, -mprefer-vector-width) (LLVM Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress