Logical Operations in RISC-V
RISC-V has a full complement of instructions to do bitwise logical operations.
Remember using &, |, <<, and >> for masking and combining in bit packing code?
These instructions implement those C-level constructs.
Basic Logic
To start with:
- Bitwise and:
and,andi - Bitwise or:
or,ori - Bitwise exclusive or (xor):
xor,xori
These are all three-operand instructions.
All of these instructions operate on all 64 bits in the registers at once.
They also all have a register version and an immediate version; the latter one has the i suffix.
The forms of the instructions are like:
xor rd, rs1, rs2
xori rd, rs1, imm
So the first version takes two register inputs, while the second takes a register and an immediate.
What About Not?
There is no (real) bitwise “not” instruction.
The reason is that ~x is equivalent to x ^ -1, i.e., XORing the value with the all-ones value.
If you spend some quality time with the XOR truth table, you’ll notice that you can think of it this way:
- The first input to the XOR is a bunch of bits. You want to flip some of these bits.
- The second input contains 1s in all the places where you want to flip the bit in the first input. Where this input is zero, leave the other bits alone.
So XORing with an all-ones value means “flip all the bits.”
Instead of a proper “not” instruction, you can use xori:
xori rd, rs1, -1
A Pseudo-Instruction
Let’s check this out using C. Here’s a tiny function that computes the bitwise not:
int not(int x) {
return x ^ -1;
}
Look at the assembly by typing:
$ rv gcc -O1 -S not.c -o -
Here are the instructions:
not a0, a0
ret
That looks like there is a not instruction!
What’s going on here?
RISC-V has a concept called pseudo-instructions.
These are not really instructions in machine code; they only appear in the assembly text.
There is no opcode for not.
The assembler will translate the line of assembly above into an xori instruction for you.
Other Pseudo-Instructions
While assembly languages mostly have a 1-1 correspondence to some processor’s machine code, pseudo-instructions are an exception. They exist to make assembly code easier to read and write without making the machine code more complicated.
Here are some other common pseudo-instructions:
mv rd, rs1: Copy the value of registerrs1into registerrd.li rd, imm: Put the immediate valueimminto registerrd.nop: A no-op: do nothing at all.
All three of these pseudo-instructions are equivalent to special cases of the addi instructions:
mv rd, rs1does the same thing asaddi rd, rs1, 0li rd, immisaddi rd, x0, immnopisaddi x0, x0, 0
Try to convince yourself that these addi instructions do in fact work to implement these pseudo-instructions’ semantics.
The RISC-V assembler translates pseudo-instructions into their equivalent real instructions for you. So you can write li x11, 42 and that will translate to exactly the same machine-code bits as addi x11, x0, 42.
You can confirm that this is true by assembling each instruction with asbin and then inspecting the bits with xxd.
Why doesn’t RISC-V implement these pseudo-instructions as real, distinct instructions? By keeping the number of “real” instructions small, it simplifies the hardware—especially the decode stage—making it smaller, faster, and more efficient. This approach embodies the reduced instruction set computer (RISC) philosophy.
Shifts
RISC-V has bit-shifting instructions to implement C’s << and >>.
Here are the ones for shifting left:
slli rd, rs1, imm: Shift left by an immediate amount.sll rd, rs1, rs2: Shift left by an amount in a register.
No surprises here. But for rightward shifts, RISC-V has twice as many versions:
srlandsrli: Shift right logical.srasrai: Shift right arithmetic.
What is the difference between an arithmetic and a logical shift? The difference is similar to the deal with sign extension and zero extension: it’s in what you do with the most-significant \(n\) bits that weren’t there before. That is, if you shift right by \(n\) bits, you just drop the original value’s least significant \(n\) bits, but what should you put for the output value’s most significant \(n\) bits? The two versions differ in their answer:
- Logical shift right: Fill in those \(n\) most-significant bits with 0s.
- Arithmetic shift right: Fill them in with copies of the sign bit.
Say, for example, that you have a register containing the negative number -3410, in two’s complement.
- If you use
sraito do an arithmetic shift right, you fill in the top bit with a copy of the original number’s sign bit, which is a 1. So the result is still negative: -1705. - If you instead use
srlito do a logical shift right, the most-significant bit of the output will be a 0. So the result will be a very large positive number.
As with sign- and zero-extension, you want to use logical right shifts for unsigned numbers and arithmetic right shifts for signed numbers.
Consider asking yourself: why is there no separate arithmetic left shift?
An Example
Imagine that x10 contains the value 0x34ff.
What does x12 contain after you run these instructions?
slli x12, x10, 0x10
srli x12, x12, 0x08
and x12, x12, x10
Try working through the instructions one step at a time. It can save time to write the values in the registers in hex, if you can imagine the corresponding binary in your head.
The result value is 0x3400.