Here is the spine, in the course mission’s own words, and every page of this section is downstream of it:
RISC-V has no flags register and no condition codes. x86-64 has
EFLAGS, AArch64 has NZCV, RISC-V has NOTHING -- so a branch is the
only conditional thing, and that single absence explains why
AArch64 needs csel/cset at all and why a compiler emits a branch on
one target and a cmov on another.
An absence is the hardest kind of claim to teach, because there is nothing to point at. A reader who has never compared the three targets has no reason to believe that CMOVcc is not merely unusual on RISC-V but absent — and the difference between those two is the difference between “clang chose a branch here” and “clang had no choice.”
And it is fully measurable without hardware, which is why this course exists on a host with no RISC-V silicon. The compiler’s choice is a static property of its output, and the output is bytes. There is no RISC-V machine here, no emulator, no RISC-V linker, and no timing anywhere in this course — but none of those are needed to establish that a mnemonic does not appear in an object file, or that one does.
What is not needed here and is worth saying plainly: this page cannot tell you which of the three targets is faster. Not the branch, not the cmov, not the mask. The x86-64 ABI course in the first section measured a 4.92x and a 27.65x on hardware it could run; this course has no counterpart for either number and does not invent one. Read the counts below as counts.
A course that inferred the absence from a compiler’s output would be arguing from a hole, and a hole is weak evidence. The manual says it outright, and saying so is what turns a negative into a measurement of a documented decision. QUOTED, rv32-unprivileged, the branch-design note:
"We considered but did not include conditional moves or predicated
instructions, which can effectively replace unpredictable short
forward branches."
Read what that concedes. It is not a claim that a conditional move would be slower. It is a claim that a conditional move can replace an unpredictable short forward branch, and that the architecture declined to let it. So the absence is a choice, and this course is measuring a choice rather than inferring one from a gap in a table.
The same note says the design went the other way deliberately, in a sentence that names the alternatives:
"The conditional branches were designed to include arithmetic
comparison operations between two registers ... rather than use
condition codes (x86, ARM, SPARC, PowerPC)."
That is the reason blt exists at all. On x86-64 a comparison is an instruction that writes a register you do not name (cmp sets EFLAGS), and a branch is a separate instruction that reads it (jl). On RISC-V the comparison IS the branch — blt a0, a1, target compares two registers and transfers control, and there is no separate instruction and no state in between.
With no CMOVcc and no SETcc to reach for, a compiler implementing x < 0 ? a : b has three moves and no fourth:
xor/sub/and. Straight-line, three instructions, and it computes both sides.?:, four levels, three bucketsThe corpus is noflags.c: eleven functions, every one of them a ?: on an integer condition, compiled at four optimisation levels and classified on the mnemonic by the decoder, out of the object file. MEASURED.
$ python3 rvabi.py --run | sed -n '/level branch/,+5p'
level branch mask other calls what is called
-O0 79 47 245 0 (none)
-O1 32 34 9 0 (none)
-O2 32 33 9 0 (none)
-Os 30 31 9 0 (none)
Read the branch column and the mask column together at -O2 and the shape is the finding: the compiler picks a BRANCH for most of the corpus and reaches for ARITHMETIC only where arithmetic is cheaper than a branch — myabs, mask, sign, absu, which are all the same three-instruction idiom, plus sign, which is one instruction. And the other column falls from 245 to 9 as the optimiser stops emitting the bookkeeping the audit is counting, which is the reader’s warning that a count over -O0 is a count of a compiler’s scaffolding.
Here is the row the course title is about, instruction by instruction at -O2, decoded from the bytes. MEASURED-ON-BYTES:
$ python3 rvabi.py --run | sed -n '/THE CONSTRUCTIVE ROW/,+22p'
function bytes instruction
myabs 4 sraiw a1, a0, 31
myabs 2 c.xor a0, a1
myabs 2 c.subw a0, a1
myabs 2 c.jr ra
mymin 4 blt a0, a1, 0x10
mymin 2 c.mv a0, a1
mymin 2 c.jr ra
mysel 2 c.beqz a0, 0x36
mysel 2 c.mv a2, a1
mysel 2 c.mv a0, a2
mysel 2 c.jr ra
mask 4 sraiw a1, a0, 31
mask 2 c.xor a0, a1
mask 2 c.subw a0, a1
mask 2 c.jr ra
sign 4 srliw a0, a0, 31
sign 2 c.jr ra
absu 4 srliw a1, a0, 31
absu 2 c.xor a0, a1
absu 2 c.subw a0, a1
absu 2 c.jr ra
sign is ONE instruction: srliw a0, a0, 0x1f, and that is the whole of (x < 0) ? 1 : 0 for a signed int. There is no SETcc to reach for and there is nothing to reach for one WITH — the comparison never left a register, it went straight into the shift’s operand. The result is a program in which the condition is a VALUE rather than a STATE, and that distinction is the whole of the difference between the three architectures.
So the absence of a flags register is not only a cost. It is what makes min, max, clamp and abs expressible without a conditional move at all. The RISC-V answer is that sltuWRITES A REGISTER, so the comparison’s result is data and the selection is arithmetic on data. On x86-64 the comparison’s result is bits in a register you do not name, so the selection has to be an instruction that reads those bits — and that instruction is CMOVcc.
Read myabs against the same C on x86-64: mov / neg / cmovs. Also three instructions, but one of the three reads flags, and the instruction that reads flags is one an architecture has to define, specify, and give a mnemonic. RISC-V’s three are sraiw/xor/sub, and none of them is conditional.
And mymin is a BRANCH: blt a0, a1, .LBB1_2, then mv, then ret. That is where a compiler with CMOVcc would have written a conditional move and this one cannot — and the interesting fact is that it does not want to. mymin is three instructions and cmov would have made it four.
Now the comparison that the whole section exists for. One source file, three --target= flags, one compiler, one optimisation level — so a row that disagrees with another row is a difference between the targets and not between the bodies of work someone typed. IT IS NOT A TIMING AND NOT A SPEEDUP, and there is no ratio anywhere in this table.
$ python3 rvabi.py --run | sed -n '/the mnemonics in the corpus/,+3p'
target the mnemonics in the corpus the idiom they are total
riscv64 blt(6), bge(4), c.beqz(5), c.bnez(1), bne(1) branches 17
aarch64 csel(14), cneg(2) conditional moves / sets 16
x86-64 cmovel(3), cmoveq(3), cmovgel(1),
cmovgl(3), cmovll(4), cmovsl(2) conditional moves 16
THE THREE SETS, DISJOINT -- computed, and printed whatever they
turn out to be:
riscv64 n x86-64 = EMPTY
aarch64 n riscv64 = EMPTY
aarch64 n x86-64 = EMPTY
union of the three = 13 distinct mnemonics; sum of the sets = 13
and every set is non-empty: aarch64=2, riscv64=5, x86-64=6
Union is 13, sum of the three sets is 13, so nothing is in two sets. The disjointness is computed, and printed whether or not it is empty, because the first version of this section wrote the word “disjoint” in a paragraph and never computed an intersection — which is the difference between a claim and a measurement, and the same failure shape as the zero-disagreement cross-check.
And note the shape of each set. RISC-V’s five are all branches. AArch64’s two are csel and cneg. x86-64’s are six cmov spellings — six rather than one because cmovll is the 32-bit “less” form and cmovll is not cmovl, and there are sixteen conditions times two operand sizes. A reader who wants to see that table is better served by a prefix match than by sixteen enumerated spellings, and the artifact matches on the prefix for a reason it documents: an enumerated table goes quietly empty the first time one is missed, and an empty table reads as “this target has no conditional move”, which is the one conclusion it must never support.
$ python3 rvabi.py --run | sed -n '/THE THREE IDIOMS SIDE BY SIDE/,+4p'
mymin a < b ? a : b
riscv64 : blt a0, a1, 0x10 | c.mv a0, a1 | c.jr ra
aarch64 : cmp w0, w1 | csel w0, w0, w1, lt | ret
x86-64 : movl %esi, %eax | cmpl %esi, %edi | cmovll %edi, %eax | retq
the idiom: riscv64 branch | aarch64 csel | x86-64 cmov
Three architectures, one C function, one compiler — and the arithmetic is not a cost either. On x86-64 the sequence for mymin is mov / cmp / cmov / ret: four instructions where RISC-V’s is three. And the reason is not that x86-64 is worse at selects:
mov because the candidate value has to be in the register the cmov will write, and it is not there yet.cmp because NZCV has to be set before csel can read it.blt compares two registers as part of the branch and there is nothing to set up.So the absence of a flags register is not uniformly a cost. It costs the compiler a CMOVcc to reach for, and it saves the mov and the cmp that CMOVcc depends on.WHICH OF THOSE WINS ON REAL HARDWARE IS NOT MEASURABLE HERE AND IS NOT CLAIMED — there is no RISC-V machine, no AArch64 machine, and no cycle measured anywhere in this course.
csel has a fourth operand at all“csel exists because NZCV exists” is only worth something if csel really is an instruction that reads a condition, and the condition is a four-bit field. The artifact decodes the AArch64 words with the AArch64 course’s own a64dec.py — a second decoder, from a different course, and neither it nor rvdec.py is a disassembler:
$ python3 rvabi.py --run | sed -n '/word.*the reader says/,+3p'
word the reader says the decoder says condition field
0x5a805400 cneg cneg w0, w0, mi bits[15:12] = 0x5 cond 5
0x1a81b000 csel csel w0, w0, w1, lt bits[15:12] = 0xb cond 11
And here is the out-of-encodings argument, counted rather than asserted. A conditional move needs three registers — two candidates and a destination — and the I-type format has three register fields: rd at bits[9:7], rn at bits[19:15], rm at bits[4:0]. A 64-bit I-type instruction is 32 bits: 7 of opcode, 3 of funct3, 15 of register, and 7 of immediate. The condition needs 4 bits and the immediate field has 7, so THERE IS NO ROOM LEFT for a condition in a word that already has three registers in it. csel is therefore a DIFFERENT ENCODING, not an instruction with an extra field — which is the strongest form the argument can take, because it is arithmetic rather than a design preference.
One parsing note, because it is the kind of failure that prints like a result. The first version of that table matched the disassembly with a regex over the whole line, and its \s+ after the instruction word consumed the tab along with the padding — so the “mnemonic” it captured was the first operand. Nothing matched, the table came out empty, and the section that exists to prove that csel is an instruction rather than a name reported no instruction and no error. The section now says how many words it found, and it splits the line on its tab.
c.mv a0, a1 and then write cc.mv a0, a1, or setl a0 if you want a diagnostic. Assemble both for riscv64. The first succeeds; the second is not an instruction on this architecture, and an assembler that rejects a mnemonic is the cheapest possible proof that a mnemonic is absent rather than merely unused. Then count the ?: in the surrounding code that could have used it.)bigsel takes fifteen arguments and makes five selects. At -O2 the compiler emits FIVE BRANCHES and spills to the stack to reach the arguments past a7. There is no masking alternative that fits in the registers it has, so the branch is not a preference — it is the only encoding left. Now write your own function with fifteen arguments and one select, and see how many branches it gets. The count of branches in a function is a measure of how much register pressure the machine has, and that is a better use of the number than a style guide is.)twice uses the SAME condition twice and collapses to a single add on all three targets — the compiler noticed the expression is commutative in the two selects and the condition vanished. Then perturb it so the two selects are not interchangeable and watch the branches come back. A corpus where every row came out the same way would be a corpus measuring nothing; a control you can break is what makes the rest believable.)Backwards, concept 1 established the register set this page uses as operands: a0–a7 and fa0–fa7 are where values arrive, so the argument registers are what a ?: operates on. rv-immediate is where blt’s displacement lives — and why it is a B format with a non-contiguous immediate, which is a cost of the same decision in the encoding.
Across architectures, a64-cond is this page from the other side. There the condition codes exist, and the interesting question is what you can do with them — which is a question about the breadth of the conditional vocabulary. Here the interesting question is what a compiler does with nothing, and the answer turns out to include the single most useful instruction in the corpus. Same subject, opposite direction: on AArch64 the flags are the resource, on RISC-V the missing resource is what shapes the code.
And x86-map is where cmovll’s actual bit layout lives, because this page counts the six spellings without explaining why there are six. The size suffix and the condition suffix are two separate things in the ModRM byte, and the reason cmovll is not cmovl is a fact about the encoding rather than about spelling. x86-flags is the other half: it is where EFLAGS is treated as the register RISC-V does not have.
Forwards, concept 3 turns the absence into a register-count claim: a callee here saves fewer registers than an x86-64 callee for the same C, because there is no flags register to save — and that is countable in the corpus.