A choice without a branch
x = c ? a : b with a branch is four words. Set x = b, beqz c, a delay
slot, and a move that the branch skips. Release 2 offers a
instead:
| instruction | does |
|---|---|
movz rd,rs,rt | if rt == 0 then rd = rs; otherwise rd is unchanged |
movn rd,rs,rt | if rt != 0 then rd = rs; otherwise rd is unchanged |
So x = c ? a : b becomes move x,a then movz x,b,c: start with a,
and if c is zero replace it with b. Two words, no branch.
x = c ? a : b, with a branch and with movz
the ch08-movz scenario
1move $t0,$a12beqz $a2,skip3nop4move $t0,$a05skip:1move $t1,$a02movz $t1,$a1,$a2Click any highlighted line to see it light up in the other listing, and read what it does.
What to notice
Both forms give the same x. The branch form has a slot to fill (or waste)
and a branch the front end must predict. The movz form runs in a straight
line: two instructions, always the same time.
Watch it, both ways
The runner computes the choice with a branch, then with movz for c = 0
and for c = 1, and ends with a seb. It pauses before the first movz.
x = c ? a : b three times: with a branch, with movz (c = 0), with movz (c = 1); then a seb
How does the compiler avoid a branch for a small choice, and what does movz do when its test fails?
program (address · word · instruction)
| PC → | 0xbfc00000 | 0x3c191000 | _start:lui $t9,0x1000 |
| 0xbfc00004 | 0x37390ff0 | ori $t9,$t9,0xff0 | |
| 0xbfc00008 | 0x2404000a | li $a0,10 | |
| 0xbfc0000c | 0x24050014 | li $a1,20 | |
| 0xbfc00010 | 0x24060000 | li $a2,0 | |
| 0xbfc00014 | 0x00a04025 | move $t0,$a1 | |
| 0xbfc00018 | 0x10c00002 | beqz $a2,0xbfc00024 | |
| 0xbfc0001c | 0x00000000 | nop | |
| 0xbfc00020 | 0x00804025 | move $t0,$a0 | |
| 0xbfc00024 | 0xaf280004 | skip:sw $t0,4($t9) | |
| 0xbfc00028 | 0x24180020 | li $t8,32 | |
| 0xbfc0002c | 0xaf380000 | sw $t8,0($t9) | |
| 0xbfc00030 | 0x00804825 | move $t1,$a0 | |
| 0xbfc00034 | 0x00a6480a | movz $t1,$a1,$a2 | |
| 0xbfc00038 | 0xaf290004 | sw $t1,4($t9) | |
| 0xbfc0003c | 0xaf380000 | sw $t8,0($t9) | |
| 0xbfc00040 | 0x24060001 | li $a2,1 | |
| 0xbfc00044 | 0x00805025 | move $t2,$a0 | |
| 0xbfc00048 | 0x00a6500a | movz $t2,$a1,$a2 | |
| 0xbfc0004c | 0xaf2a0004 | sw $t2,4($t9) | |
| 0xbfc00050 | 0xaf380000 | sw $t8,0($t9) | |
| 0xbfc00054 | 0x240b00c9 | li $t3,201 | |
| 0xbfc00058 | 0x7c0b6420 | seb $t4,$t3 | |
| 0xbfc0005c | 0xaf2c0004 | sw $t4,4($t9) | |
| 0xbfc00060 | 0xaf200008 | sw $zero,8($t9) |
registers — read · written this tick
console (what the doors printed)
▮tick 0 of 24
Nothing has run yet. The PC (program counter) points at the first instruction.
The branch form costs a beqz, its delay slot and a skipped move. The movz form is two instructions and no slot: start with x = a, then movz x,b,c copies b in only when c is 0. When c is 1 (the third block), movz changes nothing and x stays a. The seb at the end turns 201 (0xC9) into −55 by copying bit 7 upward.
What to notice
The branch version takes ticks 6 to 9, slot included. The movz version is
two ticks. In the third block c is 1, and the movz tick turns no
register green: $t2 keeps a. That is the whole trick: a failed test
leaves rd exactly as it was.
Before reading on, decide:
You want x = c ? a : b but only have movn. Which two instructions?
The old rd is an input
When the test fails, movz must leave rd as it was. In a simple core that
costs nothing: just do not write. In a core that renames registers, every instruction that may write rd
gets a fresh physical register. That register must end up holding the old
value. So the old rd is genuinely
read. Carolyne’s description lists it as a third source of movz, movn and
ins (§8.2). The runner colors it blue on every movz tick.
movz $t0,$t1,$t2 (0x012A400A): SPECIAL, funct 10
Which field is the test, and which is the value copied?
What to notice
Plain R-format: rs = $t1 is the value, rt = $t2 is the test, rd =
$t0 is written, and read. funct 10 is movz; 11 would be movn.
seb and seh: sign extension without a load
Section 4.3 showed lb sign-extending a byte as it loads it. What if the
byte is already in a register? Before release 2 the answer was two shifts:
sll rd,rt,24 then sra rd,rd,24. Release 2 added
.
seb and seh on 0x000000C9
Which bit is copied, and how far?
201 (signed) · 0x000000c9
-55 (signed) · 0xffffffc9 · 4294967241 if read unsigned
201 (signed) · 0x000000c9
- the bit that gets copied
- the copies
- the bits kept
Click any input bit to flip it.
What to notice
seb copies bit 7 upward: 0xC9 has bit 7 set, so the byte 201 becomes −55.
seh copies bit 15 upward: bit 15 of 0x00C9 is 0, so seh leaves the
value at 201. Flip bit 15 on and watch seh turn negative too.
GCC uses seb when C narrows an int to a char. In fib.c’s outchar,
the int a becomes the char parameter of print_char:
seb v0,v0 from outchar (0x7C021420 at 0xBFC00164)
How does the word say seb rather than seh?
Go deeper: how movz is described for a renaming core, and why the count is small in your dumps
In Carolyne’s MIPS description movz and movn fill three source slots:
rs, rt and rd (the operand OPR_RD_SRC3 reads the rd field as
source 3). The ALU body computes mux(rt == 0, rs, old_rd) and always
writes the destination, so the renamed physical register is always valid.
Without the third source the engine would have to know that “sometimes
nothing happens”, which is exactly the kind of instruction-specific rule the
µop contract forbids.
The census found one movz in 34 builds: GCC’s -O2 turned one small if
into it. seb appeared 19 times, always where an int met a char. These
counts are why the chapter is short. The instructions matter for reading
compiled code, not for writing much of it.
Can you now explain it?
movz and movn copy a register only when a test register is zero or not zero, leaving rd alone otherwise; a small choice becomes two straight-line instructions. seb and seh sign-extend a byte or halfword already in a register.
If you remember only three things
- Start with one answer, conditionally overwrite with the other. move x,a then movz x,b,c. The failed case is the starting value, untouched.
- The old rd is an input. In a renaming core the unchanged value must still be written; Carolyne reads rd as a third source.
- seb and seh are lb and lh without the load. Copy bit 7 or bit 15 upward; the compiler emits them when C narrows to char or short.
Now try to explain this one yourself: abs(x) without a branch. First
sra t,x,31, which gives −1 for negative x and 0 otherwise. Then xor y,x,t and subu y,y,t. Explain why this works. Where would a movn version
need negu?
Go deeper: a model answer, once you have tried
For a negative x, t is all ones: x xor t flips every bit and − t
adds 1, which is exactly two’s-complement negation. For a positive x, t
is 0: the xor changes nothing and the subtraction subtracts 0. Three
instructions, no branch, no conditional move. A movn version would compute
negu n,x first, then move y,x, then movn y,n,t: also three
instructions, but it needs the negation available as a value to copy. Both
avoid the branch; the compiler picks whichever fits the surrounding code.
Check your understanding
0/5 answered1.$t0 = 7, $t1 = 3, $t2 = 5. After `movz $t0,$t1,$t2`, what is $t0?
2.$t1 = 0x00000080. What does `seb $t0,$t1` put in $t0?
3.RV32I has no seh. How does a RISC-V compiler sign-extend a halfword already in a register?
4.Why does Carolyne's description list rd as a THIRD source of movz and movn?
5.Why would a compiler prefer movz over beqz for `x = c ? a : b`?
Next: §8.2 — bit fields and counting: ext, ins, clz, clo, rotr.