CMP-310 · Lesson 2 of 12 · 20 min
Instruction set design and RISC-V in depth
The instruction set architecture is the contract between software and hardware. It defines the instructions, registers, data types, addressing modes, memory model and exception behavior that a program may rely on, and it leaves everything else, from pipeline depth to cache size, to the implementation. A good contract lasts decades: software written for an instruction set keeps running on processors built with technologies its designers never imagined. That longevity makes instruction set design a careful balance between what compilers need, what hardware can implement efficiently, and what will still make sense in twenty years.
CMP-229 taught RISC-V as a language: the base instructions, how to write loops and procedures, and how C becomes machine code. This lesson looks at the same instruction set from the designer's side. Why are the formats laid out as they are? Why does RISC-V have only a few addressing modes? What do the optional extensions add, and what does the compressed extension do to code size? Maren needs these answers because the two NS-5 candidates implement different subsets of RISC-V, and the choice affects both the size of the firmware and the speed of the classifier.
Design choices every instruction set makes
Every instruction set answers the same questions. Where are operands kept: on a stack, in an accumulator, in registers, or directly in memory? How are memory addresses formed? Which operations and data types are supported? How are instructions encoded: at a fixed length, or at variable lengths? How do control transfers specify their targets? Early machines kept operands in an accumulator because registers were expensive. Stack machines, which take operands from the top of a stack, produce compact code and survive in virtual machines and bytecodes. Almost every instruction set designed since about 1980 is a load-store, or register-register, architecture: arithmetic operates only on registers, and only load and store instructions touch memory.
Load-store designs won because registers are fast, because compilers allocate variables to registers well, and because separating memory access from arithmetic makes every instruction take a similar, predictable amount of work. That predictability is what lets a pipeline run smoothly. Register-memory designs, in which an add can read one operand straight from memory, need fewer instructions for some code but make instructions vary widely in their work, which complicates pipelining.
| Operand style | Example of C = A + B | Strengths | Weaknesses |
|---|---|---|---|
| Stack | push A; push B; add; pop C | Very compact code; simple compiler | Stack is a bottleneck; hard to pipeline and reorder |
| Accumulator | load A; add B; store C | Simple hardware | One register means many memory accesses |
| Register-memory | load r1, A; add r1, B; store r1, C | Fewer instructions | Instructions vary in length and work |
| Load-store | lw r1, A; lw r2, B; add r3, r1, r2; sw r3, C | Uniform, pipeline-friendly instructions | More instructions; needs many registers |
CISC and RISC
In the 1970s many instruction sets grew complex: instructions that copied strings, evaluated polynomials or called procedures with elaborate frame management, each with many addressing modes. The motivation was to narrow the gap between high-level languages and hardware and to reduce memory use when memory was costly. These complex instruction set computers, CISC, needed microcode to implement their rich instructions. In the early 1980s, measurements showed that compilers rarely used the complex instructions and modes, and that a small set of simple instructions accounted for most executed code. Reduced instruction set computers, RISC, kept only simple, regular instructions that could be pipelined at one per cycle and relied on compilers to combine them.
The debate has since faded into convergence. Modern high-performance CISC processors translate their complex instructions into simple internal micro-operations and then execute those in a RISC-like out-of-order core, so the internal machinery of both families looks alike. Instruction set choice still matters for code density, decoder complexity, power and licensing, which is why RISC-V, an open instruction set designed in the 2010s, has spread quickly in embedded processors like the ones Maren is evaluating.
Addressing modes
An addressing mode specifies how an instruction locates an operand. Register mode uses a register's value; immediate mode uses a constant in the instruction; displacement mode adds a constant offset to a register to form a memory address. Measurements on many programs show that these three cover the overwhelming majority of operand accesses, with displacement covering stack variables, structure fields and most array accesses. CISC designs add modes such as register-indirect with auto-increment, scaled index, and memory-indirect. RISC-V keeps only register, immediate and displacement modes, plus PC-relative addressing for branches and jumps. Any other mode is synthesized from a short sequence of instructions, which the pipeline executes quickly.
Encoding: why RISC-V's formats look the way they do
RISC-V base instructions are 32 bits long and come in six formats: R for register-register operations, I for immediates and loads, S for stores, B for conditional branches, U for upper immediates, and J for jumps. The formats share one decisive property: the register fields rs1, rs2 and rd sit in the same bit positions in every format that has them. The decoder can therefore start reading the register file in parallel with decoding the opcode, before it even knows which instruction it has. This regularity shortens the critical path of the decode stage and is one reason a small RISC-V core can run at a good clock rate.
The price of fixed register positions is that immediates must fit around them. In the S format, used by stores, the 12-bit offset is split into a 7-bit upper part and a 5-bit lower part. In the B format, used by branches, the offset bits are also permuted so that most bits land in the same positions as in the S and I formats, and the sign bit is always bit 31. That permutation looks odd to a person reading hexadecimal, but it means the hardware that extracts and sign-extends immediates needs fewer multiplexers. The instruction set was designed for the hardware and the compiler, not for people reading machine code.
Branch and jump offsets count in units of two bytes, not one, because instructions may be 16 bits long once the compressed extension is present. A B-format branch reaches about ±4 KiB around the program counter, and a J-format jump about ±1 MiB. Longer jumps combine a U-format instruction, which sets the upper 20 bits of a register, with a jump through that register.
Worked problem
Encoding a store
Jonah is checking a disassembler bug and wants to encode sw x7, 12(x2) by hand. The store opcode is 0100011, funct3 for a word store is 010, rs2 is the data register x7 and rs1 is the base register x2. The S format places imm[11:5] in bits 31 to 25 and imm[4:0] in bits 11 to 7.
Show solution
- The offset 12 is 000000001100 in 12-bit binary, so imm[11:5] = 0000000 and imm[4:0] = 01100.
- Fields from bit 31 down: imm[11:5] = 0000000, rs2 = 00111, rs1 = 00010, funct3 = 010, imm[4:0] = 01100, opcode = 0100011.
- \[ 0000000\,00111\,00010\,010\,01100\,0100011_2 = \mathtt{0x00712623}\]
Answer: The word is 0x00712623. The disassembler had swapped the two immediate fields, so it printed an offset of 384 instead of 12.
A modular instruction set
RISC-V is defined as a small base plus optional standard extensions, named by letters. The 32-bit integer base, RV32I, has about 40 instructions. The M extension adds integer multiply and divide; A adds atomic memory operations for multiprocessors; F and D add single- and double-precision floating point; C adds 16-bit compressed encodings; V adds vector processing; B adds bit-manipulation instructions; and Zicsr and Zifencei provide control registers and instruction-fetch fences. A processor's capability is described by a string such as RV32IMC, and profiles bundle common combinations for application processors so that operating systems can rely on them.
Modularity suits embedded design. A tiny controller can implement RV32EC, a reduced base with 16 registers plus compressed instructions, while a server core implements the 64-bit base with nearly every extension. The NS-5 candidates differ exactly here: Core A implements RV32IMC, while Core B implements RV32IMFC plus a small set of packed-integer instructions useful for the radar filter. The classifier's floating-point code would run on Core A only through software emulation, at a cost of tens of instructions per operation, so the team must either rewrite it in fixed-point arithmetic or choose Core B.
| Extension | Adds | Why it matters for the NS-5 |
|---|---|---|
| M | Integer multiply, divide, remainder | Filter and checksum code multiplies constantly |
| A | Atomic read-modify-write and reservations | Needed if the node ever runs two cores or a real-time OS with shared queues |
| F, D | Single- and double-precision floating point | The prototype classifier uses single precision |
| C | 16-bit encodings of common instructions | Shrinks firmware so it fits the on-chip flash |
| V | Scalable vector instructions | Would accelerate the radar FFT and filter (lesson 11) |
| B | Bit manipulation: count, rotate, extract | Speeds packet framing and CRC code |
Compressed instructions and code density
Code density matters for small devices because on-chip memory is a large share of chip area and power, and because a denser program fits more of itself in the instruction cache. The C extension provides 16-bit encodings for the most frequent instructions when their operands meet restrictions: one of eight commonly used registers, a destination equal to a source, or a small immediate. Any compressed instruction expands to exactly one 32-bit instruction, so the extension adds only a small expander to the decode stage. Studies of compiled code typically find that compression saves somewhere between a quarter and a third of code size.
Jonah estimates the effect for the NS-5 firmware from the classifier's static instruction mix and the fraction of each class that fits a 16-bit form. The program also encodes the four instructions from this lesson, which is a quick way to check a toolchain by hand.
# Encode a few RV32I instructions and estimate code size with the C extension
OPC = {"R": 0b0110011, "I": 0b0010011, "LOAD": 0b0000011, "S": 0b0100011}
def r_type(f7, rs2, rs1, f3, rd):
return (f7 << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | OPC["R"]
def i_type(imm, rs1, f3, rd, opc=OPC["I"]):
return ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | opc
def s_type(imm, rs2, rs1, f3):
imm &= 0xFFF
return ((imm >> 5) << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | ((imm & 0x1F) << 7) | OPC["S"]
prog = [("add x10, x11, x12", r_type(0, 12, 11, 0, 10)),
("addi x5, x6, -12", i_type(-12, 6, 0, 5)),
("lw x7, 8(x2)", i_type(8, 2, 2, 7, OPC["LOAD"])),
("sw x7, 12(x2)", s_type(12, 7, 2, 2))]
for text, word in prog:
print(f"{text:20s} 0x{word:08x} {word:032b}")
# Static instruction mix of the classifier (counts) and the share that fits 16-bit forms
mix = {"ALU reg-reg": (5200, 0.62), "ALU immediate": (4100, 0.71), "load": (3300, 0.58),
"store": (1500, 0.55), "branch": (2400, 0.40), "jump/call": (900, 0.50), "other": (600, 0.0)}
n = sum(c for c, _ in mix.values())
plain = 4 * n
comp = sum(c * (f * 2 + (1 - f) * 4) for c, f in mix.values())
print(f"instructions: {n}, RV32I bytes: {plain}, with C: {comp:.0f} ({comp/plain:.1%} of original)")add x10, x11, x12 0x00c58533 00000000110001011000010100110011 addi x5, x6, -12 0xff430293 11111111010000110000001010010011 lw x7, 8(x2) 0x00812383 00000000100000010010001110000011 sw x7, 12(x2) 0x00712623 00000000011100010010011000100011 instructions: 18000, RV32I bytes: 72000, with C: 51432 (71.4% of original)
Compression would shrink the classifier from 72,000 bytes to about 51,400, roughly 71 percent of its original size. The fractions that fit 16-bit forms are estimates from a compiler listing, so the real figure will differ somewhat, but the saving is large enough that both candidates must support the C extension: without it the full firmware would not fit in the 256 KiB of flash the board allows.
How the instruction set shapes the implementation
Instruction set decisions ripple through every later lesson. Fixed-length 32-bit instructions with aligned fields make fetch and decode simple, which helps the pipeline in lesson 3. Having no condition codes, so that branches compare registers directly, removes a hidden dependence that complicates out-of-order execution in lesson 6. A clean load-store design means only two instruction types can cause memory exceptions, which simplifies precise exceptions. A relaxed memory model, which lets hardware reorder some memory accesses, gives designers freedom in lesson 10, while fence instructions let software restore order where it matters. Privilege levels and the control and status registers support the operating system and virtual memory in lesson 9.
There are also decisions RISC-V deliberately avoided. It has no branch delay slots, an earlier RISC feature in which the instruction after a branch always executes; delay slots helped one specific five-stage pipeline but became a burden for deeper and superscalar designs. It defines integer overflow as wrapping rather than trapping, leaving checks to software. Each omission was made to keep the contract from binding future implementations to today's microarchitecture.
Common mistakes
MistakeThinking that RISC means fewer instructions in a program.
InsteadRISC programs usually execute more instructions; the gain comes from a lower CPI and a faster clock.
MistakeAssuming CISC and RISC processors work completely differently inside.
InsteadModern CISC cores translate instructions into simple micro-operations and execute them much like RISC cores.
MistakeReading the B-format branch offset as if its bits were in order.
InsteadThe offset bits are permuted to keep the sign bit at bit 31 and share positions with other formats; reassemble them per the specification.
MistakeAssuming every RISC-V core supports floating point or atomics.
InsteadCheck the extension string, such as RV32IMC; features beyond the base are optional.
MistakeExpecting the C extension to speed up every program.
InsteadCompression mainly saves memory and instruction-cache space; its speed benefit depends on how much fetch bandwidth or cache capacity limited the program.
Summary and key terms
- The instruction set architecture is the software-hardware contract; the microarchitecture implements it.
- Load-store architectures dominate because uniform register operations pipeline well.
- RISC kept simple, regular instructions; modern CISC cores translate to RISC-like micro-operations.
- Register, immediate and displacement addressing cover most operand accesses.
- RISC-V keeps register fields in fixed positions, splitting immediates to do so.
- RISC-V is a small base plus optional extensions such as M, A, F, D, C, V and B.
- The C extension cuts code size by roughly a quarter to a third, which matters for small devices.
- Instruction set architecture
- The programmer-visible definition of instructions, registers, memory and exceptions.
- Microarchitecture
- The particular hardware organization that implements an instruction set.
- Load-store architecture
- A design in which only loads and stores access memory and arithmetic uses registers.
- Addressing mode
- A rule for locating an operand, such as register, immediate or displacement.
- Micro-operation
- A simple internal operation into which a complex instruction is translated.
- Code density
- How compactly a program is encoded, usually measured in bytes for a given task.
- Extension
- An optional, standard addition to the RISC-V base instruction set.
Practice problems
Work each problem on paper first, then reveal the answer to check.
Encode addi x5, x6, -12 in hexadecimal.
Show answer
The immediate is 0xFF4, rs1 = 6, funct3 = 000, rd = 5, opcode 0010011, giving 0xFF430293.
Why does RISC-V split the store immediate into two fields?
Show answer
To keep rs1 and rs2 in the same bit positions as in other formats, so registers can be read before decoding finishes.
A firmware image is 120 KiB of RV32I code and 60 percent of its instructions can be compressed. Estimate its size with the C extension.
Show answer
0.6 × 2 + 0.4 × 4 = 2.8 bytes per instruction on average versus 4, so about 120 × 0.7 = 84 KiB.
Give two reasons load-store architectures pipeline well.
Show answer
Only loads and stores access memory, so other instructions do similar, predictable work; and register operands are fast and easy for hazard detection to track.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.