RISC-V Atomics and the Vector Extension

Decode the Data Path

22 minadvancedModule 2: The Configuration Instruction, and the Data Path It ConfiguresContinues from rv-vector
22 min · Concept 4 of 4 · module: vector · RISC-V Atomics and the Vector Extension

One bit, in three instruction groups

A configuration instruction that sets a policy is not much use until you can see what the policy does. So: the mask. And the first result is that it is one bit at inst[25] in three different instruction groups — arithmetic, unit-stride load, unit-stride store — with every pair XORed and every result printed.

$ python3 rvat.py --run | sed -n '/THE ELEVEN XORs/,+16p'
  instruction  the masked form    XOR         bits  which
  vadd.vv      v1, v2, v3, v0.t   0x02000000  1     bit 25
  vadd.vi      v1, v2, 0x7, v0.t  0x02000000  1     bit 25
  vadd.vx      v1, v2, a0, v0.t   0x02000000  1     bit 25
  vle8.v       v1, (a0), v0.t     0x02000000  1     bit 25
  vle16.v      v1, (a0), v0.t     0x02000000  1     bit 25
  ... through vse64.v ...

  11 pairs, 11 of them XOR to exactly 0x02000000, 0 exceptions.
                

Eleven pairs, one value, zero exceptions. That is what makes vm a field of the word rather than of one instruction class — the word is the only thing those three groups share. And the count is eleven and not twenty-two, which is worth a sentence because the first draft of this section claimed twenty-two: it had counted three arithmetic forms at two widths when the corpus has one width per form. A count that cannot come out the same twice is a count of something else, and the cheapest test is to print the table and count the rows.

And bit 25 is “release” in an atomic and “mask agnostic” in a vector word. The two meanings are in different opcodes so they never collide — and that is a finding about bit positions in general: a bit position is a property of an instruction group, not of an architecture. The AArch64 course reached the same thing from the other direction, where bit 23 is “128-bit atomic” in one group and “acquire” in another, and it cost that course a retraction.

The mask register is not in the encoding

vm is a single bit that says “use v0”, and v0 is the only mask register the encoding can name. There is no field for it. The assembler states that property in its own diagnostic, which is a better measurement than a claim:

    vadd.vv v1, v2, v3, v0.t         ACCEPTED, 0x002180d7
    vadd.vv v1, v2, v3, v1.t         REFUSED: operand must be v0.t
    vle32.v v1, (a0), v0.t           ACCEPTED, 0x00056087
    vle32.v v1, (a0), v1.t           REFUSED: operand must be v0.t
                

A diagnostic that states a property of the hardware rather than a rule about spelling, and that cannot be misread as the latter. It is also why a decoder can name twice as many vector instructions as it has operands to print: the space of (instruction, mask register) pairs is not in the word.

And here is the part that is easy to get wrong in the other direction, so it is measured rather than asserted. A mask producer may write any vector register — vmsne.vi v1, v2, 0 assembles — and the only one of those that can then be used as a mask is v0. So the constraint is not “masks are v0” but “masks are v0 and masks are produced by ordinary vector registers”, which is a weaker claim and the correct one. The consequence is for a register allocator: keep v0 out of the general pool — not because writing it is illegal, but because anything it writes will eventually be used as a mask. That is a constraint on the allocator and not on the instruction.

The first draft of this section inferred from “v0 is the only mask register the encoding can name” that an allocator could not treat a mask as a distinct kind of thing. The first half is right and the inference is wrong, and a true premise with a false inference is harder to catch than a false premise, because the false half is the second half.

And the register group is an alignment constraint

The other half of what vsetvli buys is the group count: three bits at the top, nf, holding the number of registers minus one. A group of four is not a wider register — it is four registers the instruction addresses together, and the assembler proves it by refusing a group that does not start aligned:

    vl2r.v v1, (a0)          REFUSED
    vl2r.v v8, (a0)          ACCEPTED
    vl4r.v v8, (a0)          ACCEPTED
    vs2r.v v1, (a0)          REFUSED
    vs2r.v v8, (a0)          ACCEPTED
                

So LMUL is not a scalar and it is not a wider register; it is an alignment constraint on a register allocator, and the encoder enforces it by refusing a word. A scalar enables loop unrolling with no alignment constraint, so the three-bit nf field is the price of not having a lower bound on what a vector register is. The first draft here called the group a scalar and treated the refusal as a spelling rule; a refusal is the cheapest proof that a constraint is real, and it is also the proof that the constraint is not about syntax.

What the compiler emits, and the finding about -Os

Here is where a reader who has just come from the neutral SIMD course will be most disoriented, so the disorientation is named first. This course has no timing of any kind, and what replaces a ratio is a count — which is a weaker thing in a specific way worth stating rather than apologising for: an instruction count does not know whether the instruction is fast. It knows what the compiler did, which is a fact, and not what the hardware did with it, which was the other half.

$ python3 rvat.py --run | sed -n '/THE LOOP COUNTS/,+7p'
  function  -O0 no v  -O1 no v  -O2 no v  -O3 no v  -Os no v  -O0 +v  -O1 +v  -O2 +v  -O3 +v  -Os +v
  vadd_d    36        12        12        12        12        37      12      53 +4v  53 +4v  12
  vmac_l    37        13        13        13        13        38      13      47 +7v  47 +7v  29 +12v
  vmul_s8   35        11        11        11        11        36      11      51 +4v  51 +4v  11
  vsum_d    35        13        13        13        12        36      13      40 +4v  40 +4v  29 +9v
                

Three results, and the first one corrected a claim this course started with.

That third one is the kind of thing a first draft gets wrong by borrowing a number from a nearby table. “Smaller” and “fewer vector instructions” are two different claims, and the first version of this paragraph let the second stand in for the first. It is now retraction R-something in the artifact and limit 14 in the harness, and the sentence that carries it is code size is a different number from vector count.

The tail, which is where a runtime length becomes code

And the scalar prologue is not dead weight — it is the remainder, the loop the vector loop cannot run. The vector loop is straight-line with no branch inside it and no tail check, because the compiler has already computed the trip count as a multiple of the elements per vector:

$ python3 rvat.py --run | sed -n '/AND THE TAIL, which is where/,+9p'
    -O2     vadd_d   53 instr: 47 scalar, 1 vsetvli, 4 vector data
    -O2     vmac_l   47 instr: 37 scalar, 1 vsetvli, 7 vector data
    -O2     vmul_s8  51 instr: 45 scalar, 1 vsetvli, 4 vector data
    -O2     vsum_d   40 instr: 32 scalar, 2 vsetvli, 4 vector data
                

So the scalar count is not zero even when there are vector instructions, and that is the tail-handling code — beside the vector loop rather than inside it. Which is the answer to the portability question on the previous page, and it is not an answer to it: the tail is handled by arithmetic on the trip count and a scalar loop, which works on every implementation regardless of whether ta or tu was requested. The compiler gets portability not from the policy bits but from not depending on the tail, and a reader who sees ta, ma in the encoding has learned nothing about which of the two strategies was used.

Two readers, and the count that is not a subset

The decoder that produced every number on the previous three pages is checked by a second reader — llvm-objdump-21 — over the whole corpus. Three numbers matter, and the third is the one that is easy to forget.

$ python3 rvat.py --run | sed -n '/instructions the second reader printed/,/DISAGREE/p'
    2197  instructions the second reader printed
    2147  of them this file NAMES
      50  unmodelled -- counted, never dropped
       0  where the two readers disagree on the LENGTH

       0  DISAGREE
                

Zero disagreements, and fifty words the decoder declines to name, counted rather than dropped. That third line is the one that matters, and a sibling course in this collection shipped a cross-check that reported zero disagreements over a corpus where eighty-five instructions genuinely disagreed — because the bug had made the comparison vacuous rather than wrong. Not one of those bugs printed a bad word; they all stopped the comparison happening. A cross-check that counts agreements and one that counts holes are measuring different things, and printing only one of them is how a decoder loses a model without anybody noticing.

Fifty is an honest number for this corpus. The vector extension has several hundred encodings and this file names the three configuration instructions, the mask bit in three groups and the register group, then counts the rest. A decoder that named every vector word and got one wrong would be a worse decoder than one that says which half of the extension it implements — and the half it implements is the half these four pages make claims about.

Four poisons, and why each one moves a different column

$ python3 rvat.py --run | sed -n '/FOUR POISONS, FOUR CLAIMED/,+11p'
  FOUR POISONS, FOUR CLAIMED NUMBERS, FOUR NON-ZERO DELTAS:

  * poison 1 claims the NAMED and the UNMODELLED counts, and moved them
              by -47 and +47 -- and it claims the DISAGREEMENT count
              stays at +0, which is a claim as load-bearing as either
              of the two that moved
  * poison 2 claims the DISAGREEMENT count, and moved it by +72 while
              claiming the UNMODELLED count stays at +0
  * poison 3 claims the ORDERING-BIT POSITIONS, and moved 28 of 28 with
              0 unmoved
  * poison 4 claims the VISIBILITY of the check, and hid 1448 of 1933
                

Read poisons 1 and 2 together, because they are the pairing that makes the other two meaningful. Poison 1 removes a model and the unmodelled count rises while the disagreement count does not move — a missing model makes a hole. Poison 2 removes a different model and the disagreement count rises while the unmodelled count does not move — a superseded model makes a mistake. A cross-check that counts only agreements calls both of them a pass, because in both cases the instruction is still “compared” and the comparison found nothing to say.

And poison 4 is the one that makes the other three meaningful. The check is planted with 1,933 real disagreements and asked how many it catches: the working check catches all 1,933, and a deliberately broken normaliser — one that keeps the mnemonic and the first operand and deletes the rest — hides 1,448 of them. That is the number a reader should care about, because 1,448 is the class of bug that reads as success. A cross-check that agrees is not proof, and this is the reason: the only way to know whether a cross-check can fail is to make it fail on purpose.

The boundary: 58 claims, and eighteen of them were retractions

And the last mechanical question is the same one every course in this section ends with, and it is the reason the four pages above are checkable at all: where does the claim stop? This is that page, and it is a table and a count rather than a paragraph.

Every claim in the course carries one of three labels — MEASURED (about the compiler or the bytes, by experiment), MEASURED-ON-BYTES (a property of emitted bytes, cross-checked against the second reader), or QUOTED (a manual claim, with a document and a section) — and on most pages you would be forgiven for not noticing. The count is the finding:

$ python3 rvat.py --run | grep 'rows:'
  58 rows: 20 MEASURED, 18 MEASURED-ON-BYTES, 20 QUOTED.
                

This is the course in the whole RISC-V section with the smallest quoted third, and the reason is structural rather than a matter of care. The privileged course is 52 per cent quoted because its subject is a document; this one’s subject is an encoding and a compiler’s choice, and there was simply more to measure — the AMO encodings, the two ordering bits as an XOR, thirteen XOR probes that decompose the vector type field, eleven mask XORs, fifteen fence spellings, the -march experiment and twenty objects of compiler output. What an encoding is and what a compiler chose are exactly what a host without hardware can measure. The comparison is printed both ways in the artifact, because the honest number is the unflattering one.

Nineteen limits, and the two that decide the other seventeen

The limits are printed in the artifact’s own words so that a reader who copies a number out of the file cannot lose the sentence that limits it. The first three decide what every other one may say:

$ python3 rvat.py --run | sed -n '/^  1\. NO TIMING/,/^  4\./p'
  1. NO TIMING. No cycle, no latency, no throughput, no speedup, no ratio
     of anything a machine did. There is no RISC-V machine, no emulator and
     no RISC-V linker on this host, so every figure is a bit pattern, a
     count of bit patterns, an arithmetic identity, or a refusal from a
     real assembler. The `simd` and `smp` courses measured speedups on
     hardware, `x86simd` measured a lost-update count, and `a64simd`
     measured 3.89x, 4.92x and 27.65x; those have NO COUNTERPART HERE AND
     ARE NOT INVENTED TO FILL THE GAP.
  2. NOTHING IS EXECUTED. Not one instruction in this course has run. A
     compare-exchange in section 5 has never exchanged; it is four words a
     compiler emitted and a disassembly that named them.
  3. NO RESERVATION IS EVER HELD, NO STORE-CONDITIONAL IS EVER OBSERVED TO
     SUCCEED OR TO FAIL, NO AMO IS EVER OBSERVED TO BE ATOMIC, NO FENCE IS
     EVER OBSERVED TO ORDER ANYTHING AND NO VECTOR INSTRUCTION IS EVER
     EXECUTED. Five separate absences, and each one is a separate thing
     this course would otherwise be able to say.
                

And beside the limits there are nine things a reader therefore cannot conclude and eleven they can, printed side by side rather than one in a footnote. The second list being the longer one is the point: there is more that can be read off bytes than there is that can be said about behaviour, and a reader who finishes this course able to state the second list precisely and the first list vaguely has it exactly backwards.

Every sentence in both lists is pinned whole, by the harness. That is not fussiness: a substring check passes over a bullet edited from “That any atomic instruction is atomic” to “that any atomic instruction is probably atomic”, because the sentence keeps its subject and its verb and the edit reverses the meaning without touching a word any check was written against. A scope list you can soften without editing a test is not a scope list. The intended cost is that a reworded claim has to be re-argued in the harness too, which is the price and not a bug.

Eighteen retractions, and not one of them is about the hardware

Every one of the eighteen was asserted in a draft of this course or in the brief it was written from, measured, and withdrawn. They are printed in full rather than footnoted, and the harness asserts their text sentence by sentence — because that is the only mechanism that has ever stopped a retraction being quietly dropped. Four are worth naming because they change what a reader should expect:

And the sentence that ties the list together is the most interesting thing on the page:

$ python3 rvat.py --run | grep -A 2 'EIGHTEEN COURSES IN A ROW'
  EIGHTEEN COURSES IN A ROW, AND EVERY SINGLE RETRACTION IS A
  DISCOVERY ABOUT THE MACHINERY BUILT TO READ THE HARDWARE.
                

The RISC-V atomics, fence and vector extension did not surprise this course once. What surprised it was a two-bit field two of whose four values are invalid on half the instructions that carry it, a decoder that computed three bits where the encoding has two, a bit that is “release” in one opcode and “mask” in another, a policy the specification declines to make deterministic, and a tail policy the compiler picks the fast way by default. Eighteen courses in a row, and not one of the retractions is a mistake about how a computer works.

And the harness is a harness, not a rubric

A separate script corrupts the recorded report thirty-four ways — a bit pattern flipped, a field renamed, a claim de-negated, a retraction deleted, a nineteenth invented, a speedup ratio added, a retired link restored, and the whole file truncated to nothing — and asserts that every one is caught. That is the difference between a harness and a rubric, and it is the only part of the build that can fail while everything is green.

Try it

  1. Break a check and read the message.(Open the recorded rvat.out, change one bit pattern in one table — say 0x00b6352f to 0x00b6353f — and run python3 crosscheck.py. It fails, and it names the two checks that depended on it. Now change the sentence that explains the table and see whether it fails too, which is a different and harder thing to arrange: the first version of this harness could not do it, and the corruption script is what found out.)
  2. Count a bit position yourself, in two instruction groups.(In vec_objdump.txt, find an unmasked vadd.vv and the vadd.vv masked with v0.t immediately after it. XOR them. Then do the same for a vle32.v pair. Same number both times. Now ask what would have to be true of the hardware for the mask to be a property of one instruction group rather than of the word — and the answer is what the eleven-row table with zero exceptions is for.)
  3. Read a retraction as a bug report against a reader.(Take R9, the true-but-incomplete one. The claim was about a load, and the load really was identical; the fence count beside it was the thing that changed the meaning. Now ask the general question: in your own comparisons, when is a number a fact about what you fed in rather than about what was chosen? R12 in this list is the same bug in a different place, and the honest answer to the general question is usually “I would not have noticed”.)

Connect

Nothing here is re-taught. The neutral principles are in smp-atomic and simd-reduce; the same subject on the two machines this collection can run is x86-atomics and a64-neon. The borrowed decoder comes from rvasm’s rv-verify, and this course prepends four models to it and edits no sibling — which means one of this course’s retractions is a defect in the other course’s code, published rather than quietly patched, because a course that edits its neighbour to make its own numbers work is a course whose numbers are not measurements any more.

And the boundary this page draws is the section’s last one. The privileged course drew the same kind of line for a different subject, and reading the two tables side by side is the fastest way to understand why the quoted fraction moves with the subject rather than with the author: 52 per cent quoted when the subject is a document, 35 per cent when the subject is an encoding. Neither number is a quality score. Both are maps.