Skip to content
Contents

Conditional moves and sign-extension: movz, movn, seb, seh

Section 8.1 9 min read

  • conditional move: copy only if a register is (not) zero
  • the old rd as a third source
  • seb and seh: sign extension without a load

Checked against: MIPS32 Vol. II: MOVZ, MOVN, SEB, SEH carolyne_ref/dumps/fib_o0.dump (bfc00164 seb v0,v0) carolyne_ref/isa_mips/operand.py (OPR_RD_SRC3) carolyne_ref/isa_mips/mop.py (MOP_SPECIAL3)

Before you start

By the end, you can

  • Rewrite x = c ? a : b with movz and say why it beats a branch
  • Predict rd after movz and movn in both cases
  • Say what seb and seh do and which idiom they replace
  • Read seb in a dump as int-to-char narrowing

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:

instructiondoes
movz rd,rs,rtif rt == 0 then rd = rs; otherwise rd is unchanged
movn rd,rs,rtif 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

with a branch
1move $t0,$a1
2beqz $a2,skip
3nop
4move $t0,$a0
5skip:
with movz
1move $t1,$a0
2movz $t1,$a1,$a2

Click 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 →0xbfc000000x3c191000_start:lui $t9,0x1000
0xbfc000040x37390ff0ori $t9,$t9,0xff0
0xbfc000080x2404000ali $a0,10
0xbfc0000c0x24050014li $a1,20
0xbfc000100x24060000li $a2,0
0xbfc000140x00a04025move $t0,$a1
0xbfc000180x10c00002beqz $a2,0xbfc00024
0xbfc0001c0x00000000nop
0xbfc000200x00804025move $t0,$a0
0xbfc000240xaf280004skip:sw $t0,4($t9)
0xbfc000280x24180020li $t8,32
0xbfc0002c0xaf380000sw $t8,0($t9)
0xbfc000300x00804825move $t1,$a0
0xbfc000340x00a6480amovz $t1,$a1,$a2
0xbfc000380xaf290004sw $t1,4($t9)
0xbfc0003c0xaf380000sw $t8,0($t9)
0xbfc000400x24060001li $a2,1
0xbfc000440x00805025move $t2,$a0
0xbfc000480x00a6500amovz $t2,$a1,$a2
0xbfc0004c0xaf2a0004sw $t2,4($t9)
0xbfc000500xaf380000sw $t8,0($t9)
0xbfc000540x240b00c9li $t3,201
0xbfc000580x7c0b6420seb $t4,$t3
0xbfc0005c0xaf2c0004sw $t4,4($t9)
0xbfc000600xaf200008sw $zero,8($t9)

registers — read · written this tick

HI 0x00000000LO 0x00000000PC 0xbfc00000

console (what the doors printed)

▮
cycle 0/241 tick = 1 instruction (not a cycle)

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?

3126
opcode6b
000000
0 = SPECIAL
2521
rs5b
01001
$t1 ($9)
2016
rt5b
01010
$t2 ($10)
1511
rd5b
01000
$t0 ($8)
106
sa5b
00000
0
50
funct6b
001010
10 = movz
binary00000001001010100100000000001010
hex0x012a400aone hex digit = four bits; a digit that spans two fields has two colors
asmmovz $t0,$t1,$t2R-format
Hover or focus a field to see its bits, value and role.

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?

bit 31bit 27bit 23bit 19bit 15bit 11bit 7bit 3
rtbit 7 and bit 15 are the sign bits seb and seh copy

201 (signed) · 0x000000c9

seb: the low byte, sign-extended

-55 (signed) · 0xffffffc9 · 4294967241 if read unsigned

seh: the low halfword, sign-extended

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?

3126
opcode6b
011111
31 = SPECIAL3
2521
rs5b
00000
0
2016
rt5b
00010
$v0 ($2)
1511
rd5b
00010
$v0 ($2)
106
sa5b
10000
16
50
funct6b
100000
32 = seb
binary01111100000000100001010000100000
hex0x7c021420one hex digit = four bits; a digit that spans two fields has two colors
asmseb $v0,$v0R-format
Hover or focus a field to see its bits, value and role.

What to notice

Opcode 31, SPECIAL3, funct 32, and the sa field is 16. seh is the same word with sa = 24. This is the third place a “spare” field names the instruction, after funct (§1.3) and rs for rotr (§3.3).

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

  1. 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.
  2. The old rd is an input. In a renaming core the unchanged value must still be written; Carolyne reads rd as a third source.
  3. 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 answered
  1. 1.$t0 = 7, $t1 = 3, $t2 = 5. After `movz $t0,$t1,$t2`, what is $t0?

  2. 2.$t1 = 0x00000080. What does `seb $t0,$t1` put in $t0?

  3. 3.RV32I has no seh. How does a RISC-V compiler sign-extend a halfword already in a register?

  4. 4.Why does Carolyne's description list rd as a THIRD source of movz and movn?

  5. 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.

Where this comes back

2 later sections build on this one.