Kinds of Instruction Set Architectures
Broadly speaking, we can speak to (at least) three different kinds of instruction set architectures: Stack Machine, Register Machine, and Accumulator Machine.
Within the Register Machines, we also have subclassification, namely two-operand machines and three-operand machines.
There are also zero-operand machines — these are the stack machines, and, there are one-operand machines — these are the accumulator machines.
Commonality
Regardless of the kind of ISA as listed below, each of these has the notion of a Program Counter or Instruction Pointer. The latter is the Intel architecture term, and the former used by the other architectures. Both terms refer to the same concept, namely a CPU register whose value is the address of the memory location that holds the instruction to be executed next (if at the end of a clock cycle, or, what instruction is currently executing if at the beginning of a clock cycle).
This critical register, common to today's processors, is used to sequence the program. The program executes one instruction after another, whereby each instruction directs the processor toward some small computtation, and in addition, also directs the processor toward the next instruction's memory address, and hence the next instruction of the program. To reiterate, each instruction performs some computatation and the tells the processor what to do next by way of how it alters the program counter. This register is so fundamental to the operation of the processor what we all to often ignore it and look at sequences of instructions instead — yet this register is how sequences of instructions are determined by the (program and the) processor!
Most instructions — in particular, arithmetic & logical operations, loads & stores — will tell the processor to simply execute the next instruction in sequentially higher memory. For a MIPS processor, for example (which has fixed-size 32-bit instructions) this means incrementing the PC by 4 to get to the next sequntial instruction. There is usually no field in the instruction that says sequential execution, it is implied, and we should assume it when no other rules about next instruction are stated for a given instruction.
Some instructions purposefully alter the program counter in a non-sequential manner. Such instructions will have an explicit instruction field that tells the processor what the new PC value will be. Backward branches move the PC backwards (to lower memory addresses), which has the effect of repeating code already run — i.e. loops move the program counter backwards to previously executed instructions in order to re-execute them (presumably with different state, such as an incremented index variable)!
Other instructions purposefully advance the program counter (somewhat beyond the default of next instruction) — this is done to skip (omit) certain instructions in the program. This is done in an if-then construct, where we don't want to execute the then-part under certain conditions, so then we skip over that. It is also done for if-then-else, for example, as whenever we execute the then-part: we will also want to skip the else-part.
There are other uses for (explicit/non-default) modification of the program counter, such as function returns, indirect function calls, and virtual method dispatch in OO languages.
The program counter can also be used to locate global data, especially constants. Sometimes such data are stored near the code, and these can be accessed by specifying the distance between the instruction to execute (its address), and the address of the global or constant.
This is called pc-relative addressing, when used for either code-to-data or code-to-code access, using the delta from the current instruction to the target.
So the program counter register serves two purposes. It sequences the program so that it runs the proper instructions, including loops, and skipping ahead sometimes, it is also used as a base register to access instructions and data that are nearby the currently executing instruction.
The Kinds
Accumulator Machine
Accumlator machines are characterized by having a single accumulator. While we can generalize these as "single register" register machines, they differ from the usual register machine classification in that they use one-operand instructions!
Most instructions in an accumulator machine follow the form "operation operand" where the operation may be load, store, add, etc.., and the operand refers to a memory location.
The accumulator is both an implicit (i.e. unspecified) source and implicit target in most instruction — the accumulator is not specified in the assembly language or machine code.
On an accumulator machine, a = b + c
looks something like this:
Instruction | Comments |
---|---|
load b |
implicitly targets the accumulator for result of load |
add c |
implictly sources and then targets the accumulator |
store a |
implictly source the accumulator for value to store |
Accumulator machines were probably the earliest of the machines. They are simple and fairly regular, which makes things easy on hardware. Accumulator machines generally don't support a stack (no stack pointer), and, leave management of temporary values (of intermediate expressions) up the programmer to manage (using spills to memory).
Stack Machine
A stack machine uses a hardware managed stack for expression computation. Complex expressions are easy to encode, since the stack manages temporary values (of intermediate expressions). Stack machines have two different kinds of instructions:
They have (1) One-operand instructions that exchange data betweem memory and the expression stack: these are forms of push
and pop
. And, (2) zero-operand instructions for arithmetic, computation, indexing, etc.. which consume their sources from the stack, and return their computations back to the stack. Compuational instructions implicitly source and target the stack, so the stack isn't mentioned in assembly language or machine code.
On a stack machine, a = b + c
looks something like this:
Instruction | Comments |
---|---|
push b |
implicitly targets the accumulator for result of load |
push c |
same |
add |
implicitly sources and targets the stack |
pop a |
implicitly sources the stack for the value to store |
It is also very easy to generate simple code for a stack machine. Each variable or constant evaluated for RHS is simply pushed onto the stack. Operators, including addition, indexing, etc.. simply consume their stack-based operands and return their value to the stack. Assignment means popping a value off the stack and storing into a location.
The stack generally also simplifies procedure calling.
However, because of the constantly changing top of stack, stack machines are more difficult for hardware to make fast.
Further, stacks greatly complicate optimizing compilers. The stack is easy to use when generating simple code for an expression tree; however, optimized code does things out of order, and also reuses intermediate results, which not a great fit for the hardware expression stack.
Register Machine
Register machines have multiple registers. They might range from having 8 to 64 register. 16 and 32 are popular choices today (x64/Arm32 and AArch64/MIPS/RISC V, respectively).
Usually these days the registers are general purpose and interchangeable for programming purposes; however, to be clear there have been a number of machines that have had multiple registers that are variously dedicated to specific uses.
The 68000 subdivides its 16 32-bit registers into 8 "address" registers, and 8 "data" registers that have some overlapping features, but each set has also some features that the other half cannot engage in. For example, either set supported addition, but only the data registers supported multiplication and logical operations, like AND & OR, while only the address registers supported memory addressing (modes).
The even eariler 6502 and 8008 and a number of other 8-bit processors — which were all initially develop for the very earliest integrated circuits — had multiple registers also similarly subdivided yet, the "data" registers tended to be only 8-bits and whereas the "address" registers 16-bits. Again address registers supported (16-bit) add, while the data registers supported 8-bit multiply (if present at all), and (8-bit) logical operations like AND & OR.
On a register machine, a = b + c
looks something like this:
Instruction | Comments |
---|---|
add r1, r2, r3 | adds r2 holding b and r3 holding c and stores in r1 for a |
This simple form is possible when there are sufficient CPU registers to hold local variables at the same time as some temporary values. When a register machine does not have enough registers to do this, then the code quality suffers, since memory must be used for the overflow.
A register machine typically uses one of its registers for a stack pointer. Sometime the stack pointer is an arbitrarily chosen register as all registers are the same (MIPS/RISC V); however, on other machines the stack pointer's special uses are encoded differently than for other instructions (x86/x64, which have deciated push
& pop
instructions to work with the stack, useful for memory-based parameter passing and in function entry/exit aka prologue/eppilogue.)
Register machines typically have no dedicated expression stack, though have sufficient registers instead for holding a number of temporary and somewhat longer lived values. The code generator must manage the usage of temporary storage — these CPU registers — and the code generator has to allocate and repurpose CPU registers during expression evaluation. Instead of pushing a value onto the stack, it is loaded (or created) in a register.
Register machines are good for optimizing compilers, since they can produce and consume values out of order, as well as consume the same value more than once when desired (these are harder to do on a stack machine without adding extra code, which is also non-trivial for the hardware).
Number of operands: two vs. three
Register machines vary in the number of operands the allow for binary operators like addition. Some register machines allow three operand instructions, as in:
r1 = r2 + r3
whereas others only two operand instructions — for example:
register1 += register2
.
In instruction set architecture, everything is a tradeoff. To use a two operand system, often a mov
instrution (an assignment) is necessary:
r1 = r2; r1 += r3
.
However, in practice, the extra copy can often be avoided. Three operand instruction are larger, since they have to encode three operands. As a result of these two factors, a two operand system will generally have better code density, and code density is critical to overall performance, while a three operand system will be simpler to program and may execute fewer overall instructions.
Load / Store Architectures
Some instruction sets are load/store architectures (MIPS/RISC V) whereas others are not (x86/x64). A load/store architecture limits memory operations to these two instructions, and this simplifies the hardware while also reducing the encoding needed for addressing modes.
This article is Copyright (C) 2019, Erik L. Eidt