RISC-V Assembler: Shift
This RISC-V assembler post covers shift instructions, such as sll, srl, and srai. I also explain how to use shift instructions to quickly multiply and divide by powers of two. Shift instructions are included in RV32I, the base integer instruction set.
In the last few years, we’ve seen an explosion of RISC-V CPU designs on FPGA and ASIC, including the RP2350 found on the Raspberry Pi Pico 2. Thankfully, RISC-V is ideal for assembly programming with its compact, easy-to-learn instruction set. This series will help you learn and understand 32-bit RISC-V instructions and programming.
RISC-V Assembler: Arithmetic | Logical | Shift | Load and Store | Branch and Set | Jump and Function | Multiply and Divide | Compiler Explorer | Assembler Cheat Sheet
Left Shift
There are two left-shift instructions, sll and slli; they shift left, filling vacated bits with zeros.
sll rd, rs1, rs2 # rd = rs1 << rs2
slli rd, rs1, imm # rd = rs1 << imm
ProTip: Remember shift instructions as initialisms: sll is shift left logical.
The following examples shift a register two bits to the left:
li t0, 42 # t0 = (0000 0000 0000 0000 0000 0000 0010 1010)
li t2, 2 # t2 = (0000 0000 0000 0000 0000 0000 0000 0010)
sll t3, t0, t2 # t3 = (0000 0000 0000 0000 0000 0000 1010 1000)
slli t4, t0, 2 # t4 = (0000 0000 0000 0000 0000 0000 1010 1000)
Note how the result of shifting 42 two bits to the left is 168 (4×42).
Shift Multiplier
Left shift is equivalent to multiplying by powers of two. Using left shift for multiplication is often faster on small CPUs, particularly those without the mul instruction.
For example, shifting left by three bits multiplies by 8 because 23 = 8:
li t0, 4 # t0 = 4
slli t1, t0, 3 # t1 = 4 << 3 = 32
li t0, -4 # t0 = -4
slli t1, t0, 3 # t1 = -4 << 3 = -32
See Multiply Divide for flexible multiplication with mul.
Right Shift
There are four right-shift instructions. srl and srli fill vacated bits with zeros. sra and srai perform sign extension, filling vacated bits with the most significant bit (MSB).
# shift right logical
srl rd, rs1, rs2 # rd = rs1 >> rs2
srli rd, rs1, imm # rd = rs1 >> imm
# shift right arithmetic
sra rd, rs1, rs2 # rd = rs1 >>> rs2
srai rd, rs1, imm # rd = rs1 >>> imm
These right-shift examples use immediate instructions to contrast logical and arithmetic shifts:
li t0, 42 # t0 = (0000 0000 0000 0000 0000 0000 0010 1010)
li t1, -42 # t1 = (1111 1111 1111 1111 1111 1111 1101 0110)
# right shift by 2 bits (MSB=0)
srli t2, t0, 2 # t2 = (0000 0000 0000 0000 0000 0000 0000 1010)
srai t3, t0, 2 # t3 = (0000 0000 0000 0000 0000 0000 0000 1010)
# right shift by 2 bits (MSB=1)
srli t4, t1, 2 # t4 = (0011 1111 1111 1111 1111 1111 1111 0101)
srai t5, t1, 2 # t5 = (1111 1111 1111 1111 1111 1111 1111 0101)
Both signed and unsigned numbers are positive when the MSB (most significant bit) is 0. So, both right shift instructions fill vacated bits with 0.
When the MSB is 1, signed numbers are negative. Arithmetic shift fills vacated bits with 1, while logical shifts fill them with 0. The results are dramatically different! Treating the result as a signed number:
t4 = 1,073,741,813
t5 = -11
Always consider whether a value is signed before choosing a right shift instruction.
Learn more about the representation of signed numbers from two’s complement on Wikipedia.
Shift Divider
Right shift is (almost) equivalent to dividing by powers of two. You can divide signed and unsigned numbers by choosing the correct instruction: srli for unsigned and srai for signed.
For example, shifting right by three bits divides by 8 because 23 = 8:
li t0, 42 # t0 = 42
srli t1, t0, 3 # t1 = 42 >> 3 = 5 ; logical shift for unsigned division
li t2, -42 # t2 = -42
srai t3, t2, 3 # t3 = -42 >>> 3 = -6 ; arithmetic shift for signed division
Division is typically slow, even on CPUs with the div instruction. Right shift can significantly increase performance when dividing by powers of two. However, there is a difference: div always rounds towards zero, whereas srai rounds towards negative infinity. You have been warned! 😅
See Multiply Divide for flexible division with div and rem.
Shift Bits
RV32 shift instructions use the 5 least significant bits for the shift amount (0-31); other bits are ignored. For example, left shifting by 32 does nothing (you might expect it to set the register to zero).
GNU assembler errors if you try to shift by an immediate not in the range 0-31: Error: improper shift amount (32)
.
What’s Next?
The next post looks at RISC-V Load and Store Instructions, including addressing modes.
Check out the RISC-V Assembler Cheat Sheet and my FPGA & RISC-V Tutorials.
Share your thoughts with me on Mastodon or X. If you enjoy my work, please sponsor me. Sponsors help me create new projects for everyone, and they get early access to blog posts and source code. 🙏
References
- RISC-V Technical Specifications (riscv.org)