Skip to content
Computer Systems

The instruction set: RISC and CISC, RISC-V and assembly

RISC and CISC, RISC-V registers, instruction encoding with a bit-level example, addressing modes, a program in C and in assembly, function calls and the stack.

The instruction set architecture (ISA) is the contract between software and hardware: the list of instructions a processor can execute, how they are encoded in bits, which registers exist and how memory is addressed. A program compiled for an ISA runs on any processor implementing it, from the cheapest to the fastest. In this lesson we use RISC-V, an open, modern ISA, as the example and compare it with more complex architectures.

RISC and CISC

  • CISC (Complex Instruction Set Computer) architectures, such as the x86 of PCs, have highly varied instructions of variable length, which can operate directly on memory and do many things at once.
  • RISC (Reduced Instruction Set Computer) architectures, such as ARM and RISC-V, have few simple, regular instructions, all the same length; only load and store instructions access memory, and all calculations happen between registers.

RISC programs need more instructions, but each is simpler to decode and to execute in a pipeline: N_{instr} grows, CPI and clock period fall. Today the distinction is blurred: x86 processors internally translate complex instructions into RISC-like micro-operations, and phones and many laptops use ARM processors.

RISC-V registers

32-bit RISC-V (RV32I) has 32 registers of 32 bits, from x0 to x31. Register x0 is always zero, and writing it has no effect: it looks wasteful, but it gives many operations without dedicated instructions (copying a register is adding zero to it). By convention the others have precise roles: ra for the return address, sp for the stack pointer, a0 to a7 for function arguments, t0 to t6 for temporaries, s0 to s11 for values to be preserved.

Instruction encoding

All instructions are 32 bits long and come in a few formats, with fields in the same places, so decoding is fast. The R format is for register-to-register operations:

RISC-V R-format with the encoding of add x5, x6, x7
The 32 instruction bits are split into the fields funct7 (bits 31-25), rs2 (24-20), rs1 (19-15), funct3 (14-12), rd (11-7) and opcode (6-0). For add x5, x6, x7 the fields are 0, 7, 6, 0, 5 and 0110011, and the instruction in hexadecimal is 0x007302B3.

Example. add x5, x6, x7 (that is x_5 = x_6 + x_7): opcode 0110011 (register operation), rd = 5, rs1 = 6, rs2 = 7, funct3 and funct7 zero (add, not subtract). Lining up the bits gives the word 0x007302B3: this is the number the processor finds in memory.

The other formats: I for operations with a constant and for loads (12 bits of constant in place of rs2 and funct7), S for stores, B for conditional branches, U and J for large constants and long jumps.

Addressing modes

RISC-V has very few:

  • immediate: the constant is in the instruction, addi t0, t0, 4;
  • register: add a2, a2, t0;
  • base plus offset: the address is a register plus a constant, lw t0, 8(a0) reads the word at address a_0 + 8. This is enough to access array elements and structure fields;
  • PC-relative: branches say how far to move from the current instruction, so code works wherever it is placed in memory.

CISC architectures add richer modes (indirect, scaled index, auto-increment), which save instructions but complicate the hardware.

An example program

The sum of the elements of an integer array. In C:

int sum(int *v, int n) {
    int s = 0;
    for (int i = 0; i < n; i++)
        s += v[i];
    return s;
}

In RISC-V assembly, with a0 = array address, a1 = number of elements, result in a0:

sum:    li    t1, 0          # s = 0
loop:   beqz  a1, done       # if n == 0 exit
        lw    t0, 0(a0)      # t0 = *v
        add   t1, t1, t0     # s += t0
        addi  a0, a0, 4      # v++ (an int takes 4 bytes)
        addi  a1, a1, -1     # n--
        j     loop
done:   mv    a0, t1         # return value
        ret

You can see the compiler’s work: the index has disappeared, replaced by a pointer that advances by 4 bytes; the loop uses five instructions per element. Each instruction does a single thing, and that is precisely why the processor can execute several of them overlapped.

Function calls and the stack

jal ra, f jumps to function f saving the return address in ra; ret (that is jalr x0, 0(ra)) goes back. If a function calls another, it must save ra and the s registers it uses in a memory area managed as a stack, the stack: space is made by subtracting from sp and restored at the end. The rules on which registers to preserve form the calling convention, which lets separately compiled functions, even in different languages, call each other.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x
()
x