Keyboard Shortcuts

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

Thirty-Two Registers, Four Ways to Read One

26 minadvancedModule 1: The Vector Register File, and the Bit That Moves Per Group
26 min · The AArch64 Data Path: NEON, Atomics and Ordering

Why this matters

Open a disassembly of AArch64 vector code and you will meet the same five names over and over: b, h, s, d, q. They are not five register files. They are five views of the same thirty-two 128-bit registers, and the suffix says which view — which is a reinterpretation, never a conversion.

[QUOTED] There are 32 vector registers, each 128 bits, and an instruction names a VIEW of one: the same 16 bytes read as 16 bytes, 8 halfwords, 4 words or 2 doublewords. (ARM DDI 0487, "A64 Advanced SIMD", the section on register size.)

That is the easy half, and it is the half that produces a comfortable wrong model. The comfortable model is "one register, five widths, the suffix picks one" — which is true of the register file and false of the encoding. Because the encoding is not obliged to put the width in the same place twice, and Advanced SIMD does not:

instruction       word         binary                                the three size bits
  ---------------- ----------- ------------------------------------ --------------------------
  ldr b0, [x0]     0x3d400000  00111101010000000000000000000000     size=0 Q=0 L=1
  ldr h0, [x0]     0x7d400000  01111101010000000000000000000000     size=1 Q=0 L=1
  ldr s0, [x0]     0xbd400000  10111101010000000000000000000000     size=2 Q=0 L=1
  ldr d0, [x0]     0xfd400000  11111101010000000000000000000000     size=3 Q=0 L=1
  ldr q0, [x0]     0x3dc00000  00111101110000000000000000000000     size=0 Q=1 L=1
  str b0, [x0]     0x3d000000  00111101000000000000000000000000     size=0 Q=0 L=0
  str h0, [x0]     0x7d000000  01111101000000000000000000000000     size=1 Q=0 L=0
  str s0, [x0]     0xbd000000  10111101000000000000000000000000     size=2 Q=0 L=0
  str d0, [x0]     0xfd000000  11111101000000000000000000000000     size=3 Q=0 L=0
  str q0, [x0]     0x3d800000  00111101100000000000000000000000     size=0 Q=1 L=0

  [MEASURED-ON-BYTES]  ldr b0 and ldr q0 differ in EXACTLY ONE BIT.
  ldr b0         xor ldr q0         = 0x00800000  -> bit 23
  [MEASURED-ON-BYTES]  ldr b0 and str b0 differ in EXACTLY ONE BIT.
  ldr b0         xor str b0         = 0x00400000  -> bit 22
                

One bit. ldr b0 is 64 bits of access and ldr q0 is 128, and the entire difference is bit 23 — not bit 30, not bit 31, and nowhere near the top of the word where a reader's expectation sits.

Why this matters beyond one instruction: a decoder that has one "vector size" field is a decoder that has one measurement and two answers. Write the field map once, at bit 30, because that is where the three-same group puts it, and every vector load in every object file you will ever read is decoded at the wrong width — silently, because the result is a legal instruction and nothing anywhere complains.

The measurement: three bits, five values

So for the single load/store form the layout is bits[31:30] = element size, bit 23 = Q, bit 22 = load-or-store. The access size is therefore three bits wide with five allocated values out of eight — B, H, S, D from the two-bit size field, plus Q for 128. And the two-bit size field and the Q bit are not adjacent:

  bit   31 30 | 29 28 27 26 25 24 | 23 | 22 | 21          10 | 9      5 | 4      0
        size   |      class bits         | Q  | L  |     imm12     |   Rn   |   Rt

  b0:  0 0       0 0 1 1 1 1 0 1         0    1     0 0 0 0 0 0 0 0 0 0  0 0 0 0 0 0  0 0 0 0 0  0 0 0 0 0 0
  q0:  0 0       0 0 1 1 1 1 0 1         1    1     0 0 0 0 0 0 0 0 0 0 0  0 0 0 0 0 0  0 0 0 0 0 0
                

The float groups do the same thing with a two-bit type field, and this is where the comfortable model breaks for the second time. fadd s0, s1, s2 and fadd d0, d1, d2 differ in bit 22 alone:

instruction         word         binary                                bits[23:22]
  ------------------ ----------- ------------------------------------ ----------------
  fadd s0, s1, s2    0x1e222820  00011110001000100010100000100000     0
  fadd d0, d1, d2    0x1e622820  00011110011000100010100000100000     1
  fmov s0, w0        0x1e270000  00011110001001110000000000000000     0
  fmov d0, x0        0x9e670000  10011110011001110000000000000000     1
  umov w0, v0.s[0]   0x0e043c00  00001110000001000011110000000000     0
  umov x0, v0.d[0]   0x4e083c00  01001110000010000011110000000000     0
  fmov s0, wzr       0x1e2703e0  00011110001001110000001111100000     0

  [MEASURED-ON-BYTES]  fadd s0 and fadd d0 differ in bit 22 alone, so
  the floating-point TYPE is bits[23:22] with S = 00 and D = 01.
                

So the float suffix is a type, and the type is two bits with two of its four values unused by the baseline assembler. Which brings us to the first refusal, and the first time the assembler teaches you something about the architecture.

The third type value, hidden by a refusal

     fadd h0, h1, h2                    (baseline)             REFUSED: instruction requires: fullfp16
     fadd h0, h1, h2                    -march=armv8.2-a+fp16  0x1ee22820  fadd h0, h1, h2
     fadd q0, q1, q2                    (baseline)             REFUSED: invalid operand for instruction
     fadd b0, b1, b2                    (baseline)             REFUSED: invalid operand for instruction
     fmov h0, w0                        (baseline)             REFUSED: instruction requires: fullfp16
     fmov h0, w0                        -march=armv8.2-a+fp16  0x1ee70000  fmov h0, w0
     scvtf s0, d0                       (baseline)             REFUSED: instruction requires: fprcvt
     fcvtzs s0, d0                      (baseline)             REFUSED: instruction requires: fprcvt

  `fadd h0, h1, h2` is REFUSED at baseline with a diagnostic that NAMES
  THE FEATURE: "instruction requires: fullfp16".  With
  `-march=armv8.2-a+fp16` it assembles to 0x1ee22820, and
  0x1ee22820 xor 0x1e222820 is 0x00c00000 -- bits 22 AND 23.
                

So the encoding has a third type value, a four-bit window at bits[23:20], and half of that window is unused by the instructions a baseline assembler will produce. That is the shape of every architectural extension: the encoding is allocated generously and the assembler is stingy, and only the first of the two is checkable with a hex editor.

Now the refusal that is not about a feature, and it is the one worth reading twice:

     fadd q0, q1, q2    ->  REFUSED: invalid operand for instruction
     fadd v0.4s, v1.4s, v2.4s    ->  0x4e22d420
                

fadd q0 does not exist, and the reason is the first table on this page. The floating-point groups have a type field, and 128 bits is not a type — it is a different encoding, six bits away:

R2, and the sentence the whole page is for CLAIMED "a NEON instruction is one instruction with five widths" MEASURED the FP and Advanced SIMD arithmetic groups have a TYPE field of bits[23:22], and the 128-bit form is a DIFFERENT ENCODING: fadd s0, s1, s2 0x1e222820 fadd v0.4s, v1.4s 0x4e22d420 six bits apart WHY the suffix reinterprets, but only WITHIN a type. 128-bit arithmetic is a different GROUP, not a different suffix.

A reader who took "the suffix picks the width" to mean "one instruction, five widths" has the wrong mental model, and it is worth being wrong about exactly here — because the wrong model produces a decoder that reads fadd v0.4s with the float type field, gets a plausible word, and prints a plausible instruction.

What this measurement cannot show

It cannot show that the five views are the same bits at run time. There is no AArch64 machine here, so no value was loaded, no reinterpretation happened and no lane was read. What is measured is that five names are five 32-bit words which differ in a total of two bit positions — which is a fact about the encoding and a necessary, not sufficient, condition on the views being one register.

It cannot show a throughput, a latency, a register-file size in bytes, or whether a particular core has 32 vector registers or fewer. [QUOTED] The architectural register file is 32 × 128 bits; the number of physical registers a particular implementation has is not in the architecture at all.

The trap this page is built around

Concept 2 sweeps the same four element sizes through four different instruction groups and prints all four field positions side by side. Here is the whole of it, so you can see what the trap looks like before you fall into it:

THE SAME FOUR SIZES, IN FOUR GROUPS `ldr size` is bits[31:30] and `ldr Q` is bit 23 `add v size` is bits[23:22] and `add v Q` is bit 30 `ld1 size` is bits[11:10] and `ld1 Q` is bit 30 `sve size` is bits[23:22] and has NO Q bit at all So the same four element sizes live at TWO places in the 32-bit word, and the 128-bit form is a bit in three of the four groups and ABSENT from the fourth.

And the fourth row is the one the next concept is about, and the reason SVE has no Q bit is not an omission: an SVE vector is not a fixed 128 bits, so a bit that said "128" would be a lie. Concept 2 measures what the instruction says instead, which is nothing.

Worked: reading a field map off an XOR

The method is one line long: assemble an instruction, change one operand, assemble it again, and every bit that moved belongs to that operand. A sweep is necessary, not decorative — a single pair marks the bits where two values differ, which is a subset of the field.

field             xor mask    the positions           base instruction
  ---------------- ---------- ---------------------- ----------------------------
  ldr size         c0000000   bits[31:30]            ldr b0, [x0]
  ldr Q            00800000   bits[23:23]            ldr b0, [x0]
  ldr L            00400000   bits[22:22]            ldr b0, [x0]
  ldr Rt           0000001f   bits[4:0]              ldr q0, [x0]
  ldr Rn           000003e0   bits[9:5]              ldr q0, [x0]
  ldr imm12        003ffc00   bits[21:10]            ldr q0, [x0, #0]
  add v size       00c00000   bits[23:22]            add v0.16b, v1.16b, v2.16b
  add v Q          40000000   bits[30:30]            add v0.8b, v1.8b, v2.8b
  ldr q0, [x0, #0]           the base of the imm12 sweep
  movi imm8        000703e0   bits[5,6,7,8,9,16,17,18]  movi v0.4s, #0
  ld1 size         00000c00   bits[11:10]            ld1 {v0.16b}, [x0]

  45 field positions measured over 47 cases, 2 refusals.
                

Look at movi imm8. The mask is not a span — it is bits[9:5] and bits[18:16] with thirteen bits missing in the middle, because cmode lives there. An eight-bit immediate that is not contiguous is a fact about the design here, and evidence about the measurement everywhere else, and the only way to tell the two apart is to have swept.

And look at ld1 size = bits[11:10]. That is nowhere near the top of the word, it is in the middle, and it is the element size of a structure load. Same four sizes. Different field. A decoder that has one "vector element size" constant has one measurement and two answers, and it will be wrong on whichever group it did not sweep.

Apply it

  1. Hex-dump the five words yourself. Assemble ldr b0,[x0] … ldr q0,[x0] into an object file and XOR the first and the last. (Expect 0x00800000, one bit, bit 23. Then XOR ldr b0 against str b0 and expect 0x00400000, bit 22. If you get a different bit, your assembler is not clang 21.1.8 and the rest of this course's numbers will not reproduce — which is worth knowing before you blame the course.)
  2. Find the Q bit in the wrong place on purpose. Take 0x3d400000, set bit 30, and disassemble. (Expect the second reader to print something that is not ldr q0 — and then find out what bit 30 does mean here, which is a question worth ten minutes. The three-same group's Q bit and the load's Q bit are two different bits in two different groups, and being able to say which is which is the page's whole content.)
  3. Ask the assembler for the three refusals.fadd q0,q1,q2, fadd b0,b1,b2, fadd h0,h1,h2. (Expect "invalid operand", "invalid operand", and "instruction requires: fullfp16". The first two are a fact about the ENCODING — 128 bits is not a float type — and the third is a fact about the ASSEMBLER'S FEATURE SET. Two different kinds of refusal two lines apart, and telling them apart is the exercise.)
  4. Sweep one group yourself and find a field the sweep cannot find. Take add v0.4s, v1.4s, v2.4s and change only the destination register. (Expect bits[4:0]. Then ask: which bit in that word did the sweep not move, that you know must be a field? There is at least one — and a field your sweep never moved is a field you have not measured, however confident the table looks.)

Connect

Backwards, into the machine course. The register concept established the thirty-one integer registers and the thirty-two vector registers as two naming schemes over one physical file, and this page is where the encoding of the second scheme goes wrong. The encoding course's field-map concept is the direct parent of the sweep at the bottom of this page, including the lesson that a mask is a lower bound and that the encoding course's own imm16 was measured one bit too wide by a sweep with too few steps.

Forwards, immediately. The shared memory space takes the "same four sizes, different position" table and follows it into SVE, where a group spends no bits at all on the question every other group spends a bit on. Decode the data path is where all of this is cross-checked by a second reader, and where the field positions on this page are asserted as exact values by a harness that reads a recorded run rather than re-measuring.

Outward, and this page's claim generalises past AArch64. A field's position is a property of an instruction group, and a group boundary is something you learn by sweeping, not by reading the top of the word. x86-64's VEX prefix has the same property in a different shape: vmovups and vmovupd differ in a prefix bit that is a different bit for every instruction that has one. And the generalisation that costs a learner the most: an encoding is allocated generously and an assembler is stingy, so "the assembler refuses it" is evidence about the assembler and very weak evidence about the encoding. Four refusals on this page, two kinds, and telling them apart is the habit worth taking out of it.

render_lesson_js(&mut page)