Branch on compare
An if in C becomes a goto and a goto becomes one RISC-V branch, because the comparison is inside the branch instruction. The condition you invert, the else if chain, and what to write when the machine has no conditional move.
The overview of this topic is in Assembly basics. The same topic in M68K, Z80.
An if in C becomes a goto, and on RISC-V a goto becomes one instruction, because the comparison
happens inside the branch. There is no flag to set first and nothing between the two lines to get in
the way.
The condition you write is the one you invert
Flattening an if was the Assembly basics lecture on branching, and the shape it arrived at is
this: jump over the true branch when the condition is false.
int x = 50;
if (x > 10) {
x = 100;
} else {
x = 200;
}
int x = 50;
if (x <= 10) goto else_branch;
x = 100;
goto end;
else_branch:
x = 200;
end:
The > in the C became a <= in the flat version, because the branch is taken when the if is
not. In RISC-V that inverted condition is the mnemonic you write:
t0 comes out at 100. Change li t0, 50 to li t0, 5 and it comes out at 200 instead.
The j end is the flat version's goto end, and leaving it out is the most common bug in hand
written control flow: the program runs the true branch, walks straight into the false one and the
second answer wins.
Which branch says which condition
Six of these are real instructions and the other four are the same six with the operands swapped, which "Comparing without flags" takes apart. What matters when you are writing a program is the table.
| the C condition | signed | unsigned |
|---|---|---|
a == b | beq a, b, label | the same |
a != b | bne a, b, label | the same |
a < b | blt | bltu |
a <= b | ble | bleu |
a > b | bgt | bgtu |
a >= b | bge | bgeu |
a == 0 | beqz a, label | the same |
a != 0 | bnez a, label | the same |
a < 0 | bltz a, label | never true |
a > 0 | bgtz a, label | bnez |
Every one of them is one instruction and every one takes a label, and the assembler works out
the distance. A branch reaches about 4 kilobytes either way, which is a thousand instructions, so in
practice you write the label and forget about it. Past that the build says
Branch target word address beyond 12-bit range and you jump to a j instead, which reaches a
megabyte.
The right hand column is the reminder to pick the family your numbers belong to. An address or a
count of bytes compared with blt is being read as a signed number, and one of them above two
billion comes out negative.
else if
else if is a second comparison at the label the first branch fell through to, and every answer ends
with a jump to the end.
char grade;
if (score >= 90) grade = 'A';
else if (score >= 60) grade = 'B';
else grade = 'C';
t2 comes out at 00000042, which is 0x42, the ASCII code of B. The li t1, 90 and
li t1, 60 are there because every RISC-V branch compares two registers: there is no branch with
a constant in it, and the assembler will not put one there for you, so a comparison against a number
costs a li first.
Try changing li t0, 75 to li t0, 95 and to li t0, 12 and watching t2.
An if with no branch at all
MIPS has movn and movz, which copy a register only if a third one is or is not zero, so a
two-way choice between two values needs no jump. RISC-V has no conditional move, so the same
thing is done with arithmetic: slt gives you a 0 or a 1, sub from zero turns that into a mask
of all zeroes or all ones, and two xors pick between the operands.
t2 comes out at 1, t3 at FFFFFFFF and t4 at 3, the larger of the two. The xor, and, xor
is the standard trick for choosing without jumping: a ^ ((a ^ b) & mask) is a when the mask is
zero and b when it is all ones.
Five instructions where a branch takes three, so this is what you write when the jump itself is what
you are trying to avoid, which on a real pipelined chip is a branch the CPU cannot predict. When you
only want the 0 or the 1, slt on its own is already the whole if.
Testing one bit
The M68K has btst, which tests a bit and sets a flag. RISC-V has no such instruction and no such
flag, so you compute the and into a register and branch on whether it came out zero.
t2 comes out at 1, because bit 3 of 1010 is set, and t4 at 0, because bit 0 is not. andi with a
single bit set is C's x & 8, and beqz on the answer is C's if (!(x & 8)). The constant of
andi is 12 bits, so a bit above 11 has to go into a register with li first, or be brought down
with a shift.
For a bit whose position the program worked out, shift instead: srl t1, t0, t2 brings bit t2 down
to the bottom and andi t1, t1, 1 keeps it.
j, jal and jr
Three ways to go somewhere unconditionally, and all three are the same two instructions underneath.
j labelisjal zero, label: jump, and throw the return address away.jal labelisjal ra, label: jump, and write the address of the next instruction intora, which is a call.jr t0isjalr zero, t0, 0: jump to the address in a register, which is how a subroutine returns (retisjr rawith the register spelled out) and how a program jumps through a table of addresses it computed.
A label is an address like any other, so a .word can hold one, and lw and jr between them turn
a switch into a table lookup.
t1 comes out at 30 and t4 at 0040002C, the address of case2, which the assembler wrote into
the third word of table and the program read back out. Try changing li t0, 2 to li t0, 0 and
watching t1 come out at 10.
A jump table is one load and one jump whatever the number of cases, where a chain of bge costs two
instructions per case it walks past. What it does not do is check the index: a t0 of 7 reads a
word past the end of the table and jumps to whatever was there.
Your turn
The test starts t0 at -7. Leave its sign in t1: -1 when t0 is negative, 0 when it is zero and
1 when it is positive. For -7 that is -1.
Show solution
The second one has the three cases and the table written for you, and the test starts t0 at 1.
Work out the address of the case and jump to it, so that t1 comes out at 20.