MIPS is one of a number of processors developed in the early 1980s, partly in response to existing instruction sets like IBM 360, VAX, and Intel 8086. The other most prominent architecture of the time is probably RISC, developed by Patterson at UC Berkeley. MIPS was developed by Hennessy at Stanford. Both of these instruction sets were developed with a goal of being able to implement them using the technology of the time with a CPI of one instruction per cycle. Both came pretty close.
MIPS, oddly enough, is not an acronym for ``Millions of Instructions Per Second.'' When originally developed, it was actually an acronym for ``Microprocessor without Interlocking Pipeline Stages'' (surely one of the more tortured acronyms that's been developed!). We'll see later that this means that it was the programmer's (actually the compiler's) responsibility to avoid some weird behavior in the pipeline. Interlocks were later added, so now MIPS isn't an acronym, it's just a name (like RCA is no longer an acronym for Radio Corporation of America).
If you're accustomed to a machine like the Intel 32-bit architecture (IA-32) or the HC11, the first thing that strikes you about the MIPS is the number of registers it has: 32 general-purpose integer registers. Unlike IA-32 or HC11, there are very few special-purpose registers -- there is no specific stack pointer, nothing reserved for pointers, and so forth.
There are a few registers that do have a special purpose: PC, register $0, register $31, and the hi and lo registers.
mov $1, $2
(copy register $2
into $1) because we can do a
add $1, $2, $0
instead. In effect,
a decision was made to add the functionality of several two-operand
instructions by reducing the size of the register set slightly
instead of by increasing the size of the instruction set.
It has all those registers because the intent (unlike HC11 or IA-32) is that you should do as much arithmetic as possible in the registers. In particular, local variables for a procedure will almost always be in registers. It uses a three-operand instruction set; when you do arithmetic, you can specify two source registers and a destination register, as in
to add register $2 to register $3, and put the result into register $1.add $1, $2, $3
On the other hand, all arithmetic has to involve registers or 16-bit integers. You can't add a memory location to a register; you can't add a large constant integer to a register (you can add a 16-bit constant integer to a register, though -- and that includes virtually all constants actually used in a program).
While the 30 registers other than $0 and $31 don't have architected special purposes, there are conventions regarding their use and aliases used by the assembler, as given in the following table:
Register | Alias | Customary Use |
---|---|---|
$0 | $zero | Constant 0 (architected) |
$1 | $at | Reserved for assembler |
$2 | $v0 | Function result |
$3 | $v1 | |
$4 | $a0 | Function argument |
$5 | $a1 | |
$6 | $a2 | |
$7 | $a3 | |
$8 | $t0 | Temporary (not preserved) |
$9 | $t1 | |
$10 | $t2 | |
$11 | $t3 | |
$12 | $t4 | |
$13 | $t5 | |
$14 | $t6 | |
$15 | $t7 | |
$16 | $s0 | Temporary (preserved) |
$17 | $s1 | |
$18 | $s2 | |
$19 | $s3 | |
$20 | $s4 | |
$21 | $s5 | |
$22 | $s6 | |
$23 | $s7 | |
$24 | $t8 | Temporary (not preserved) |
$25 | $t9 | |
$26 | $k0 | Reserved for kernel |
$27 | $k1 | |
$28 | $gp | Pointer to global variables |
$29 | $sp | Stack pointer |
$30 | $fp | Frame pointer |
$31 | $ra | Return address (architected) |
There is only one addressing mode: indexed. If you want to do
something fancy like pushing something onto the stack in memory, you
have to do it yourself. You can also do immediate mode operations
using a 16 bit operand. How do you generate a 32-bit operand?
That actually takes two instructions: one (a lui
)
to load the high-order 16 bits, and one to load the low-order 16
bits.
MIPS is a byte-addressed machine, but loading or storing a word-size data element requires it to be word-aligned. This simplifies memory system design.
No condition codes! Instead, the branch family works by comparing two registers against each other for equality (or inequality), or comparing a register against 0. This is done rather than a general comparison of two registers against each other as it can be implemented more quickly.
Two features about the instruction formats immediately show MIPS to be a RISC, not a CISC:
In the case of the MIPS, all instructions are 32 bits. The three formats are:
Field Size | 6 bits | 5 bits | 5 bits | 5 bits | 5 bits | 6 bits |
---|---|---|---|---|---|---|
R-format | op | rs | rt | rd | shamt | funct |
I-format | op | rs | rt | address/immediate | ||
J-format | op | target address |
Some notes:
R-format is the MIPS's arithmetic format, and is used for nearly all the instructions in a program. One curious thing to notice is that the order of operands in assembly code does not match the order in the machine code; in assembly code you write
add $rd, $rs, $rt
while in the machine code the order is as shown in the table.
R-format is also used for the jr
instruction, which jumps
to an address specified by a register.
Just about anything that isn't R-format is I-format: it's used for nearly any instruction that involves a constant, including loads, stores, immediate arithmetic, and conditional branches.
MIPS conditional branches will come as a surprise, in that they don't use condition codes. Instead, MIPS compares two registers against each other, or compares a register against 0, and branches based on the result of the comparison. The only valid register-register comparisons are "equal" and "not equal"; the only valid single-register comparison is "greater than or equal to zero" and "less than zero". This will turn out to be very helpful in implementing out of order execution later.
A very, very weird thing about MIPS is that it uses "delayed branches" (a fact that the text, and even the assembler, hide as well as possible. The text only mentions it on page 76 and then it turns up in chapter 6). We'll ignore it too for the moment.
Finally, for those few cases where a long immediate operand absolutely can't be dispensed with (namely, jumps), MIPS provides the J-format.
Here's a little bit of code; how would we code it for MIPS?
j = 0; for (i = 0; i < 0x12345678; ++i) j = i + j;
Our goal here is more to show off the instruction set than to write
good code, so we won't work really hard at creating good code. In
fact, we'll produce pretty bad code!
We'll assume j
is a local variable in memory (see what
I mean about not creating good code?) and i
is in a
register. We'll reconstruct our constant every time through the
loop. We'll ignore the pipeline-exposing weirdness of the MIPS
(we'll pretend there's no delayed load or delayed branch). We'll
use the aliases (like $zero
) for register names instead
of the real names (like $0
).
As usual, our first step will be to translate the for
loop into an equivalent while
loop, giving us:
j = 0; i = 0; while (i < 0x12345678) { j = i + j; i = i + 1; }
Now then: we need to set j
to zero. If we assume a
symbol j
containing the offset of the variable from the
frame pointer $fp
we can do this with
sw $zero, j($fp)
Next, set i
to zero. We'll assume (for no good reason)
that i
is in register $t3
, and we can
clear it with
add $t3, $zero, $zero
Now we go on to the while
loop. We'll need to set a
label so we can get back to it, and construct a branch around the
loop if we're done. To do this latter, we need to
Construct a constant in a register. This can be done with
loop: lui $t4, 0x1234 ori $t4, $t4, 0x5678
Compare i
to that constant.
slt $t5, $t3, $t4
Take the branch if i
is greater.
beq $t5, $zero, done
Finally, we have the loop body and a branch back to the top. In the
loop body, we have to read j
, add i
to it,
store it back, and increment i
. We get
lw $t4, j($fp) add $t4, $t3, $t4 sw $t4, j($fp) addi $t3, $t3, 1 beq $zero, $zero, loop done: