Keyboard Shortcuts

Ctrl+K
Open search
Esc
Close search / dialog
↑
Back to top

SSE and SSE2: The XMM Register and the Alignment Split

26 minadvancedModule 1: The Vector Registers and What the Encoding Changed
26 min · The x86-64 Data Path

Why this matters

The SIMD course measured that a wider vector is not automatically faster, and it did it with a body the compiler chose. This concept is the part that makes a vector body unsafe to choose: the difference between an instruction that works on any address and one that dies on most of them is a single letter in the mnemonic, and the letter is the only thing standing between a working program and a SIGSEGV that depends on where an allocator happened to put an array.

There is exactly one measurement in this course that observes that requirement directly, and it is not a duration. It is ten forked children, one instruction each, at an address eight bytes off a cache line, and the output is a signal number per row. That choice is the concept: alignment is a correctness requirement and a duration table is the wrong instrument for a correctness requirement, because the difference between a working load and a faulting one is not a slowdown you can amortise away. It is a program that does not run.

The model: eight moves, and what the letters mean

The table is PRINTED AND NOT MEASURED, which is a real distinction here and not a formality: the fault half of every row is measured in the next section, one forked child per row, and the encoding half is quoted from the SDM and then checked byte-for-byte in the last concept.

$ ./vecdump | sed -n '/1B1\./,/THE NAMING IS NOT/p'
  MOVE | op   | pfx | ALIGNED | what the ALIGNED form faults on
  MOVAPS| 28  | none| 16 byte | #GP if the address is not 16-aligned
  MOVUPS| 10  | none| none   | nothing; the unaligned form
  MOVDQA| 6F  | 66  | 16 byte | #GP; the integer twin of MOVAPS
  MOVDQU| 6F  | F3  | none   | nothing
  MOVSS | 11  | F3  |  4 byte | #GP if not 4-aligned
  MOVSD | 11  | F2  |  8 byte | #GP if not 8-aligned
  MOVLPD| 13  | 66  |  8 byte | #GP if not 8-aligned
  LDDQU | F0  | F2  | none   | a LOAD that explicitly never faults
                

The naming is not decorative and it is the fastest way to remember the table. aps is aligned packed single, apd is the double, and dqa is double quadword aligned — the integer twin of movaps with the same requirement and a 66 operand-size prefix in front of it. Wherever an a appears, the instruction faults on a misaligned address. Wherever a u appears, it does not. That is the entire rule, and it holds for all eight rows above.

Three rows in that table are not about 16 bytes and are the ones people forget:

And the prefix column is where the trap in the ABI course's last concept reappears in a new costume. 66 is not a direction flag and not a size flag in the general case — in the legacy encoding space 66 is an operand-size override, and for a mov it turns a 32-bit operation into a 128-bit one. That is why movdqa is 66 0f 6f and movdqu is f3 0f 6f, and why movdqa and movaps are the same instruction wearing a prefix and a name.

The measurement: ten probes, and what they prove

The buffer is mmap'd, so the compiler knows nothing about its contents and cannot fold a load into a constant. Each probe is one instruction in its own forked child, so a fault is a signal number rather than a dead artifact.

$ ./vecdump | sed -n '/^  ALIGN |/,/vmovdqa load   | aligned/p'
  ALIGN | vmovapd load   | aligned: returned  | +8 bytes: SIGSEGV   | legacy encoding
  ALIGN | vmovupd load   | aligned: returned  | +8 bytes: returned  | legacy encoding
  ALIGN | vmovapd store  | aligned: returned  | +8 bytes: SIGSEGV   | legacy encoding
  ALIGN | vmovupd store  | aligned: returned  | +8 bytes: returned  | legacy encoding
  ALIGN | vmovdqa load   | aligned: returned  | +8 bytes: SIGSEGV   | legacy encoding
  ALIGN | vmovdqu load   | aligned: returned  | +8 bytes: returned  | legacy encoding
  ALIGN | movaps load    | aligned: returned  | +8 bytes: SIGSEGV   | legacy encoding
  ALIGN | movups load    | aligned: returned  | +8 bytes: returned  | legacy encoding
  ALIGN | vmovaps load   | aligned: returned  | +8 bytes: SIGSEGV   | VEX encoding
  ALIGN | vmovdqa load   | aligned: returned  | +8 bytes: SIGSEGV   | VEX encoding

  THE RESULT, and it is not a timing table at all:
    Every ALIGNED form faults at +8 bytes and every unaligned
    form returns -- LEGACY AND VEX ALIKE.
                

Read the last two rows before reading anything else on this page. vmovaps and the VEX form of vmovdqafault exactly like their legacy twins, and this course was written asserting that they would not. That claim is retraction 15, and it is the commonest false statement about AVX: that the VEX prefix removed the alignment requirement. It did not.

WHAT AVX ACTUALLY RELAXED, and it is a much smaller claim than the one everybody quotes. NOT THIS "VEX removed the alignment requirement." It did not. vmovaps faults on the same addresses movaps faults on. THIS the FUSED memory operand -- vaddps ymm0,ymm1,[rbx] is one instruction, and the legacy form was a LOAD then an ADD, and the load could fault on an address the add never saw. Plus: the move instructions were relaxed for unaligned STORES in the AVX encoding.

And the SDM says the thing the measurement found, in as many words: the memory operand “must be aligned on a 16-byte (128-bit version), 32-byte (VEX.256) or 64-byte (EVEX.512) boundary or a #GP will be generated.” The #GP is the interesting half, because it becomes a SIGSEGV in a user process and looks identical to an out-of-bounds access. A misaligned movaps is indistinguishable, from the kernel's point of view, from a wild pointer — and that is a real cost of the aligned form that has nothing to do with speed.

One row deserves its own note. vmovdqa load appears twice in that table — once with a legacy prefix and once with a VEX one — and they are two probes that share a spelling. A harness that counted names would collapse them to one and conclude the table had nine rows. The partition this concept asserts is over probes, and the harness checks that the two sets of five and four sum to ten without any row appearing in both.

The trap the alignment rule hides

Here is the thing that makes this rule cost real money. A misaligned movaps raises #GP, the kernel turns that into a SIGSEGV, and the resulting core dump points at a wild pointer. So the failure mode of a vectorised loop over a correctly-sized buffer looks exactly like the failure mode of a buffer overrun — and the two have completely different fixes.

There is a second trap, and it is the one that cost this course a retraction. The obvious way to write the probe is in C:

$ gcc -O2 -mavx2 -S probe.c | grep -A2 'vmov\|movap'
        vmovaps  (%rax),%ymm0        # the compiler chose the ALIGNED form
                

You asked for a load and got a faulting load, and on a machine where the buffer happens to be 16-byte aligned the experiment passes and teaches you nothing. Worse, the “m” constraint on a double[4] makes gcc emit a safe sequence instead, so the instruction you meant to test is not in the binary at all — and a probe that was never executed reports “returned” just as cleanly as one that was. That is retraction 11, and the build script here answers it by disassembling every probe body and letting the harness assert the mnemonics are present. A probe that returns must have run, and the only way to know is to look at the bytes.

The general form is worth carrying: when you are measuring a property of one instruction, write that instruction yourself and then prove it is the one that ran. Everything else in this course is downstream of that habit.

Apply it

  1. Reproduce the ten probes.cd courses/x86simd/assets/samples && ./build_samples.sh, then read section 2. (Expect six rows to fault and four to return, and expect the two VEX rows to be the surprise. On a machine whose SDM section differs, the vmovaps row is the one that tells you your expectation was wrong rather than the machine.)
  2. Break the probe the way it was broken before. Write the aligned load in C as *(volatile __m128 *)p and let the compiler pick the instruction, then disassemble the body. (Expect vmovaps or vmovdqa, and expect the “m” constraint on a double[4] to make gcc emit a safe sequence instead — which is retraction 7 in the SIMD course's compiler concept and the reason the probe bodies here are hand-written asm. A probe that silently uses a different instruction than the one you meant to test is the commonest failure in this whole collection.)
  3. Find the four-byte and eight-byte arms. Take the same probe body and substitute movss and movsd for the packed forms. (Expect them to return at +2 bytes and fault at +4 relative to an 8-byte boundary, and then work out what a compiler does when it has a 4-byte-aligned struct and a 16-byte-aligned local. The alignment of a struct member is a language question, and the answer propagates straight into a fault.)
  4. Write the check your compiler will not do for you. Take a real vector loop and add, before it, an assertion that the pointer is 16-byte aligned. (Expect it to pass almost always and to catch a real bug about once a month, which is a bad ratio for an assertion and a good one for the alternative. Then memalign the buffer and watch the assertion become dead code that the optimizer removes — and notice that this is a real fix and also a way of making the compiler stop telling you about the bug.)
  5. Measure the thing this concept says is not a speed difference. Time movups against movaps on data that is already 16-byte aligned. (Expect almost no difference, and expect a difference on hardware where the unaligned form crosses a cache line — which is why the aligned form exists and why “the aligned form is faster” is true on some machines and not on yours. The requirement is unconditional; the cost is not.)

Connect

Backwards, the SIMD course's width concept measured that doubling the width did not double the throughput, and its boundary concept is where a tail is handled. This page is the safety half of the same territory, and it deliberately does not repeat the width experiment. The SMP course owns what happens when two cores touch the same aligned line at once; the last three concepts here are that, for x86-64 specifically.

Forwards. The #GP that a misaligned aligned-form load raises is the same fault the third course's inventory counted as si_code 128, so the mechanism is one you have already met as a number. What this page adds is that a fault can be produced by a correctly bounded access to a correctly sized object, which is a category the address-space model alone does not have. The next concept is what the encoding did to the same registers, and its first finding is that VEX did not touch this rule at all.

Outward. The aligned/unaligned split is the single most common place where a portable program stops being portable, and it is worth noticing who picked the requirement: the a forms were added in 1999 for machines that could not load across a cache line cheaply, and every instruction since has inherited the letter. The compiler concept in the SIMD course is where a language decides which of these eight rows your program actually executes, and in every case the answer was “not the one you wrote.”

render_lesson_js(&mut page)