Every architecture in this collection has an instruction set, and you have learned two of them as lists: x86-64 has a list, and AArch64 has a list. RISC-V is the odd one out, and the oddness is a design decision with a measurable consequence. The base is 40 instructions and everything else is a letter.
That sentence is usually delivered as a slogan about “modularity” and it is worth exactly nothing until you ask what it does to a compiled object. So this concept asks: compile the same eleven functions of ordinary C at eight different -march settings, and diff what appears and disappears. The answer is not “M adds multiply” and it is not monotonic in “more features”, and two of the rows are the sharpest things in the course.
Why it has to come first, before any of the encoding: because a decoder is written against a set of documents, not against an architecture. Every guard in the artifact’s dispatch table belongs to one letter. When you write a decoder you are answering “does this word belong to the M extension or to the base?” and the answer is a value in a funct7 field, which is concept 2’s subject. The letters are not a packaging story; they are a dispatch story.
The base count is QUOTED, from the unprivileged manual, section 2.1, and the manual is unusually honest about it:
“RV32I contains 40 unique instructions, though a simple implementation might
cover the ecall/ebreak instructions with a single SYSTEM hardware
instruction that always traps and might be able to implement the fence
instruction as a nop, reducing base instruction count to 38 total.”
Forty. Compare that with the corpus’s own opcode space, which concept 2 counts: ordinary C touches 19 of the 128 possible seven-bit opcodes. The base is small on purpose and the rest of the space is the letters.
But the interesting string is not the -march you typed. It is the one the object file carries, and the object is what a linker reads. Tag_RISCV_arch is read out of .riscv.attributes by this file’s own ELF reader, and the rows below are MEASURED-ON-BYTES:
$ python3 rvdec.py --run | sed -n '/-march=rv64imafdc$/,/^$/p'
rv64i2p1_m2p0_a2p1_f2p2_d2p2_c2p0_zicsr2p0_zmmul1p0_zaamo1p0_
| zalrsc1p0_zca1p0_zcd1p0
names: rv64i m a f d c zicsr zmmul zaamo zalrsc zca zcd
Six letters in, twelve names out. And the four the user did not type are not toolchain decoration:
c2p0 appears andzca1p0 and zcd1p0 appear. The C extension has been split into named sub-extensions, and an object records the ones it actually uses. The reason is not tidiness: a linker has to know whether the compressed instructions in this object are decodable by the machine it is producing, and c is not an assumption a linker can make. This is QUOTED from the manual’s own history and the string is MEASURED from the file.zmmul1p0 is the multiply-only subset of M. A machine with Zmmul satisfies an object that claims it without implementing divide. So m in a -march string is a shorthand the object has to expand, and the expansion is what a linker checks.zaamo1p0 and zalrsc1p0 are the two halves of A, splittable independently for the same reason: one is the atomic memory operations and the other is load-reserved/store-conditional, and a machine may have either. That is a subject for a later course in this section, but the encoding of the split is a fact about this one and it is in the string above.zicsr2p0 is the CSR set, and rv64i alone does not name it.rv64if does. The tool adds zicsr as soon as any floating point is present, which is a toolchain decision and not an architectural requirement — and it is exactly the kind of thing this string exists to record. The manual’s own words, section 2.1: a machine including the privileged architecture “will also require the 6 CSR instructions in the Zicsr extension”. The base no longer contains them.zvl32b, zvl64b and zvl128b — the vector length bounds, in bytes. An object built for a 128-bit vector is not runnable on a 64-bit-vector machine without recompiling, and this string is how a linker learns that. There is no equivalent on the integer side, because every RISC-V integer machine is RV32 or RV64 and says so in the base.So “the ISA is a set of independent documents” is not a slogan here. It is a string in the object file, and the string is longer than the -march that produced it.
The same corpus.c — arithmetic, masks, conditionals, a loop, a struct walk, a multiply, a divide, a float accumulator, a call — compiled eight ways. MEASURED, and a compiler version is in the numbers, so read the columns and not the third digit:
$ python3 rvdec.py --run | sed -n '/-march insns/,/^$/p'
-march insns bytes 2-byte B/insn distinct mnem
rv64i 146 584 0 4.000 28
rv64im 114 456 0 4.000 27
rv64if 146 584 0 4.000 28
rv64imf 114 456 0 4.000 27
rv64imafd 95 380 0 4.000 29
rv64imafdc 95 246 67 2.589 29
rv64gc 95 246 67 2.589 29
rv64gcv 178 524 94 2.944 49
The mnemonic count barely moves — 27 or 28 for every non-vector setting — and the instruction count falls and rises with the letters in a way that is not monotone in “more features”. Read the rows as pairs: what did this letter add, and what did it make unnecessary?
rv64i -> rv64im insns 146 -> 114 bytes 584 -> 456
GAINED 3: divuw(1) mul(2) rem(1)
LOST 4: j(1) jr(1) sext.w(2) srli(2)
An extension that adds three instructions and removes four. The base has no multiply, so the compiler synthesises one out of shifts and adds, and a shift-based multiply is a sequence, and sequences branch. Adding M removed branches: j, jr, sext.w and srli all disappear, and the instruction count falls by 32.
“An extension adds instructions” is true of the ISA and false of the object, and the object is what a linker sees. This is the first of the three retractions on this page, and it is worth a moment: the same claim in the AArch64 section would have been true, because AArch64’s base has mul and its extensions add capabilities the base genuinely lacks. The claim is not architecture-neutral, and a course that states it as if it were would be teaching a fact about one ISA and calling it a principle.
rv64if 146 584 0 4.000 28
rv64i 146 584 0 4.000 28
Identical instruction count, identical byte count. The reason is in the corpus: its only floating-point function accumulates a double, and a double needs D. So the F extension buys this corpus absolutely nothing.
This is a correct measurement and it is a warning about the method, which is why the artifact keeps it rather than dropping the row. An -march sweep over a corpus that does not use a feature reports the feature as worthless. A sweep is a measurement of a corpus’s use of the extensions, and the corpus has to be in hand when you read it. The next row is the one that shows F is not worthless:
rv64imf -> rv64imafd insns 114 -> 95 bytes 456 -> 380
GAINED 4: fcvt.l.d(1) fld(1) fmadd.d(1) fmv.d.x(1)
LOST 2: jalr(3) sd(4)
What the compiler was doing before was calling out to a software floating-point library — each call is a jalr — and storing double-width intermediates on the stack, which is four sd. With hardware double-precision floating point those become registers. The instruction count fell by 19 and the byte count by 76, and the letter that did it is one character long.
And the retraction inside the retraction: the first draft of this paragraph said “three auipc disappear”. They do not. The calls are within the ±2 GiB an auipc can reach, so the address-forming pair survives and only the jalr goes. Concept 4 measures that reach.
rv64gc -> rv64gcv insns 95 -> 178 bytes 246 -> 524
GAINED 20: and(3) beq(3) bgeu(3) csrr(3) divu(2) j(3) neg(3)
srli(6) vadd.vv(1) vfmul.vv(1) vfmv.f.s(1)
vfredosum.vs(1) vl2re64.v(2) vlseg2e64.v(1) vmacc.vv(1)
vmv.s.x(3) vmv.v.i(2) vmv.x.s(2) vredsum.vs(2) vsetvli(4)
Twenty mnemonics gained, none of which is an integer instruction, and the instruction count nearly doubles. That is not a regression and it is not a measurement of the vector unit’s speed — it is what happens when a compiler auto-vectorises a loop it was previously leaving scalar, and the vector code is longer than the scalar code was. MEASURED while scoping: a plain double loop under -march=rv64gcv came out as seven vadd and one vsetvli, and the scalar form was longer in bytes.
Which is also the architecture’s sharpest idea, and the reason the count goes up: vsetvli programmes the element width, the register-group count and the tail and mask policies at runtime, so one fixed 32-bit encoding describes a vector of a length the instruction does not contain. Concept 5 is where that encoding is measured.
This is a large fraction of what this page is for, so it gets its own section rather than a caveat at the end.
clang, or a different corpus.c, will move every one of them, and the artifact says so in the paragraph under the table. What is a property of the architecture is the last column of the first table: the set of documents each object claims to need.B/insn is bytes per instruction and it is a property of the emitted file, not a density in any performance sense. The x86-64 section has speedup ratios; they have no counterpart here and are not invented to fill the gap.mul does not mean the machine cannot multiply; it means this compiler did not need to. A -march sweep measures a corpus’s use of an ISA, and an empty column is a fact about the corpus.li and addi are the same bits; concept 5 reconciles the two printers’ conventions, and the count of mnemonics moves if the printer changes its mind about which alias to print.cd courses/rvasm/assets/samples && ./build_samples.sh --only, then clang --target=riscv64-linux-gnu -march=rv64i -S -O2 corpus.c and again with -march=rv64im. Expect the second to be SHORTER, and expect the j/jr to be gone. Then find the shift-and-add sequence the base used — it is four or five instructions where mul is one.)zca/zcd, zmmul, zaamo/zalrsc, zicsr. Then change one letter and watch which names move — rv64gc adds zifencei over rv64imafdc and changes nothing else.)float function — not a double one — and rebuild at rv64if against rv64i. Expect flw, fsw, fadd.s and a software-conversion call to appear, and expect the D-row’s story to repeat at a smaller scale. Then revert, because the corpus is a shared input and the committed numbers were taken with the committed corpus.c.)Backwards, the ELF course taught you to find .text; the Tag_RISCV_arch string on this page is in .riscv.attributes, which is the same container holding a different kind of fact, and finding it is the same exercise with a different name. The reloc course is where the consequences of this page’s last section live: an attribute string exists because a linker has to refuse to produce something it cannot run.
Forwards, concept 2 takes the letters apart at the bit level — and finds that M, the extension this page measured as “fills a funct7 value the base left empty”, is not even in the same opcode space as the instruction that shares its format. Concept 3 is about the one extension whose cost is a permanent reservation of encoding space rather than a set of instructions.