Everyone cites the C extension as the thing that makes RISC-V small. That is half of it, and the half everyone cites is the half that is a compiler’s decision rather than a property of the encoding. The same eleven functions, with and without the letter:
$ python3 rvdec.py --run | sed -n '/rv64imafd -> rv64imafdc/,/^$/p'
rv64imafd -> rv64imafdc insns 95 -> 95 bytes 380 -> 246
THE INSTRUCTION COUNT IS THE SAME. 95 and 95. Adding C did not change what the program does, how many operations it performs, or which operations those are. It changed the number of bytes, and the byte count is the only thing in this concept anyone will quote about compression.
That is worth stating carefully, because the usual claim goes the other way round — that compression makes the code “smaller”, as if the program had less in it. It does not. The program has the same instructions in it; some of them have a shorter encoding. And the shorter encoding is not free: it was bought with a reservation of encoding space, and with a register field that cannot name eight of the thirty-two registers. Those are two different costs, they are measured separately, and this page is mostly about them.
The method here is exhaustive and it is cheap. All 65,536 half-words, written into a .text section as data, disassembled by the second reader, and classified twice: once by this file’s reserved rules and once by whether the reader prints a mnemonic. A two-independent-classification check over an exhaustive set cannot be vacuous — a vacuous comparison of 49,152 items would have to compare nothing.
First the boundary, because it is the thing the plan got wrong, and the correction is worth stating plainly:
CORRECTION TO THE PLAN, AND IT IS A BIG ONE. The plan for this
section said: "the number of reserved 16-bit patterns you can enumerate and
verify against the assembler". Measured, the answer is that the assembler
REFUSES 16,384 of the 65,536 half-word patterns -- exactly a quarter,
exactly bits[1:0] == 0b11 -- and it refuses them with the diagnostic
instruction length does not match the encoding
which is not a reservation message at all. It is the assembler saying
"that pattern is a 32-bit instruction, and you asked me for two bytes".
So there are two different questions and the plan conflated them:
$ python3 rvdec.py --space | sed -n '/quadrant funct3/,/^$/p'
quadrant funct3 code points RESERVED HINT rules here
q0 000 8 -
q0 100 2048 -
q1 001 64 -
q1 011 32 c.lui
q1 100 128 c.srli, c.srai
q2 010 64 -
q2 011 64 -
q2 100 1 c.mv, c.add
TOTAL 2409
2,409 of 49,152, which is 4.90 per cent of the reachable compressed space. Read that against the quarter the assembler refuses and the plan’s framing is not merely wrong but misleading. And the distribution is the real finding: one cell — quadrant 0, funct3 = 100 — accounts for 2,048 of the 2,409, which is 85 per cent. The QUOTED reason is in the manual’s own opcode map, which prints two columns for the RV32 and RV64 variants and marks that cell Reserved in both. The cell held C.FLWSP in the RV32 draft; the RV64 column needed the slot for C.LD; and when the two columns were merged the cell was given up entirely. 2,048 code points — one twenty-fourth of the whole reachable compressed space — for one decision about one quadrant.
And five of the eight reserved cells are one-off rules that kill a cell because of a register value or an immediate value. All the same shape, and the shape is the point:
sp, and adding zero is a different instruction. Not eight: the eighth, 0x0002, was given to C.SLLI64 by the RV128 work. One reserved code point is a code point somebody else claimed.0x8002 — rs1 = 0 with rs2 = 0, the single combination that C.JR, C.JALR, C.EBREAK, C.MV and C.ADD all reject. It is the only reserved compressed encoding a person is likely to meet in a hex dump, and it is reserved because five rules each exclude it. Its neighbour 0x9002 is C.EBREAK, one bit away, and a decoder that reads the cell without reading inst[12] reports a legal instruction as reserved.Two independent classifications of the same exhaustive set. “Mine” is what this file’s transcribed rules say; “oracle” is whether the second reader prints a mnemonic:
$ python3 rvdec.py --space | sed -n '/This file, over all 49,152/,/VERDICT/p'
This file, over all 49,152 reachable half-words:
2409 RESERVED by the rules transcribed above
426 HINT -- defined, and they do nothing
46317 neither: an instruction this file does not model
The second reader, over the same 49,152:
49152 instructions printed
2408 printed <unknown>, i.e. it could not decode them
THE CROSS-CHECK, over an exhaustive set:
2408 this file says RESERVED and the reader says <unknown>
1 this file says RESERVED and the reader NAMED it
0 the reader says <unknown> and this file does not
The 1 words this file calls reserved that the reader names:
0x0000 unimp
2,408 of 2,409 agree, and the one that does not is 0x0000. It gets its own paragraph rather than a footnote, because it is not a typo.
0x0000 is in the C.ADDI4SPN cell with nzuimm = 0, which this file calls RESERVED — which is what the rule says and what the manual says — and the second reader prints unimp. Both are right about their own subject, and the disagreement IS the interesting part.unimp is not a claim that the encoding is an instruction. It is the second reader’s way of saying “there is nothing here and I will keep decoding”, and it has to say something: a disassembler walks a byte stream carrying no length information, so every half-word in it must print something. The all-zeros pattern is the natural choice for “nothing” precisely because it is the one pattern no instruction is. The second reader has taken a position on what to PRINT; this file took a position on what is DEFINED. A check that compared the two would be comparing a table of encodings against a table of output strings.
Which is why this section is allowed to finish on the word DISAGREE, and why a verification that cannot finish disagreeing is not a verification.
The mask table in concept 2 says something the eye needs help with: c.lw and c.ld have the IDENTICAL mask. Both are 0x00001c60, both move five instruction bits, and they are not the same encoding. A mask records where a value’s bits live. A permutation records which instruction bit holds which bit of the value. Those are different questions, and the compressed extension is a case where the first is not enough.
$ python3 rvdec.py --section 4 | sed -n '/^ family /,/DISTINCT/p'
family variants status the permutation, derived
c.lw (x4) 32 DERIVED v[0]=inst[ 6] v[1]=inst[10] v[2]=inst[11] v[3]=inst[12] v[4]=inst[ 5]
c.ld (x8) 32 DERIVED v[0]=inst[10] v[1]=inst[11] v[2]=inst[12] v[3]=inst[ 5] v[4]=inst[ 6]
c.sw (x4) 32 DERIVED v[0]=inst[ 6] v[1]=inst[10] v[2]=inst[11] v[3]=inst[12] v[4]=inst[ 5]
c.sd (x8) 32 DERIVED v[0]=inst[10] v[1]=inst[11] v[2]=inst[12] v[3]=inst[ 5] v[4]=inst[ 6]
c.fld (x8) 25 DERIVED v[0]=inst[10] v[1]=inst[11] v[2]=inst[12] v[3]=inst[ 5] v[4]=inst[ 6]
c.fsd (x8) 25 DERIVED v[0]=inst[10] v[1]=inst[11] v[2]=inst[12] v[3]=inst[ 5] v[4]=inst[ 6]
c.lwsp (x4) 64 DERIVED v[0]=inst[ 4] v[1]=inst[ 5] v[2]=inst[ 6] v[3]=inst[12] v[4]=inst[ 2] v[5]=inst[ 3]
c.ldsp (x8) 64 DERIVED v[0]=inst[ 5] v[1]=inst[ 6] v[2]=inst[12] v[3]=inst[ 2] v[4]=inst[ 3] v[5]=inst[ 4]
c.swsp (x4) 64 DERIVED v[0]=inst[ 9] v[1]=inst[10] v[2]=inst[11] v[3]=inst[12] v[4]=inst[ 7] v[5]=inst[ 8]
c.sdsp (x8) 64 DERIVED v[0]=inst[10] v[1]=inst[11] v[2]=inst[12] v[3]=inst[ 7] v[4]=inst[ 8] v[5]=inst[ 9]
c.j 28 DERIVED v[0]=inst[ 3] v[1]=inst[ 4] v[2]=inst[ 5] v[3]=inst[11] v[4]=inst[ 2] v[5]=inst[ 7] v[6]=inst[ 6] v[7]=inst[ 9] v[8]=inst[10] v[9]=inst[ 8] v[10]=inst[12]
11 families measured, 7 DISTINCT PERMUTATIONS.
Eleven families, seven distinct orders. Not a rounding of eleven — these are the seven the eleven fall into: c.lw with c.sw; c.ld with c.sd, c.fld and c.fsd; and the four sp forms and c.j each alone. Read the first two rows against each other, because they are the pair this section opened with: same five positions, and c.lw puts the offset’s bit 0 at inst[6] while c.ld puts it at inst[10]. Same positions, different order, and the order is the entire difference between a 4-byte-scaled and an 8-byte-scaled displacement.
So the thing to carry forward is not “the compressed immediates are scrambled” — the manual says that and it is obvious. It is that a decoder for the compressed extension cannot be written by REUSING the base one, by parameterising it, or by deriving it from a table of bit positions. It needs a per-instruction permutation, and the permutation is a property of the pair (field, instruction), not of the field. A field map — which is what concept 2 measured, and what every field map in every architecture in this collection is — is necessary and not sufficient here, and the mask table above cannot tell you that and this table can.
The QUOTED reason, which is the same sentence the base gives, is worth reading because it explains why the specification is willing to pay:
“The immediate fields are scrambled in the instruction formats instead of
in sequential order so that as many bits as possible are in the same position
in every instruction, thereby simplifying implementations.”
SAME position is what it got. Same position, seven orders, and an implementation needs seven multiplexers — which is the trade, stated by the people who made it, and quantified above.
2,409 is the encoding-space cost. This is a different cost, and it is not the reservation fraction. A set of instructions that cannot be shortened at all, for a reason that has nothing to do with how much space was reserved and everything to do with where the register numbers are. And it is a refusal you can read:
$ python3 rvdec.py --section 4 | sed -n '/c.mv ra, a0/,/sw t0/p'
c.mv ra, a0 2 bytes 0x80aa mv ra, a0
c.mv zero, a0 2 bytes 0x802a c.mv zero, a0
c.lw t0, 0(a0) REFUSED invalid operand for instruction
c.lw a0, 0(t0) REFUSED invalid operand for instruction
c.lw s0, 0(a0) 2 bytes 0x4100 lw s0, 0x0(a0)
c.sw t0, 0(a0) REFUSED invalid operand for instruction
c.sd sp, 0(a0) REFUSED invalid operand for instruction
c.addw t0, a0 REFUSED invalid operand for instruction
sw t0, 0(a0) 4 bytes 0x552023 sw t0, 0x0(a0)
Four of those nine are REFUSED by a real assembler, and the refusals are the measurement.c.lw t0, 0(a0) is not encodable in two bytes and lw t0, 0(a0) is four, and the difference is a three-bit register field. t0 is x5; the field indexes x8–x15. A compiler that wants to compress a load from t0 cannot, and the four bytes are the honest price.
And the exception that proves the rule, because it is how a function prologue gets compressed at all:
c.sdsp ra, 8(sp) 2 bytes 0xe406 sd ra, 0x8(sp)
sd ra, 8(sp) 2 bytes 0xe406 sd ra, 0x8(sp)
c.sdsp t0, 8(sp) 2 bytes 0xe416 sd t0, 0x8(sp)
c.swsp ra, 8(sp) 2 bytes 0xc406 sw ra, 0x8(sp)
Note the second row: sd ra, 8(sp) is also two bytes, because the assembler chose the compressed form for the base instruction too. The reason is in the row above: C.SDSP is a CSS format with a five-bit rs2 at inst[6:2], and ra is x1 which fits. C.SD in quadrant 0 is a CS format with a three-bit rs2' at inst[4:2], and x1 does not fit there.
So the compressed extension CAN save a register it has no compressed form for, and it does so by adding a whole extra family of instructions whose only difference is that the base register is hardwired to x2. Nine formats became eleven. That is the mechanism, stated as a measurement, and it is why the compressed fraction in a real corpus is what it is rather than higher.
.hword for a handful of them, assemble, and confirm the assembler accepts every one — a reservation is enforced by the hardware and the disassembler, not by the assembler. See retraction R3.)printf ".text\\nc.lw t0, 0(a0)\\n" > t.s; clang --target=riscv64-linux-gnu -march=rv64gc -c t.s -o t.o — expect “invalid operand for instruction”. Then c.lw s0, 0(a0) and expect success, and expect the successful one to be two bytes. The difference between the two lines is three bits of register field and nothing else.)rvdec.py, make c.ld use c.lw’s permutation. Expect offsets 0–124 to decode correctly and everything above to be wrong — and expect the wrong values to be multiples of 8, in range, and plausible. That is the specific failure section 4D exists to describe, and it is why the permutations are measured rather than read out of a figure.)Backwards, concept 2 ended on the three-bit rd' field whose value 0 means x8, and this page is what that costs. Forwards, concept 4 is about the base formats’ immediates and the one that is genuinely permuted — the B type, whose highest bit lives in the position the S type uses for its lowest. Concept 5 is where the reservation count is cross-checked against a second reader over all 49,152 reachable patterns, and where the cross-check’s own normalisation is poisoned.
Outside this course: the reserved-space question and the reloc course are neighbours rather than relatives. A reserved encoding is a promise to a linker that the bit patterns it will never emit are free, and that is a relocation-shaped problem.