The M Extension: Multiply, Divide, and the Rules for Divide-by-Zero and Overflow

Sunhong··17 min read·0 views·Translated from Korean

Key points in three lines

  • The M extension adds 4 multiplies (mul mulh mulhsu mulhu), 4 divides (div divu rem remu), and 5 W instructions on RV64. The lower half of a product is the same regardless of sign, so there is one mul; the upper half depends on interpretation, so there are three.
  • Division by zero and signed overflow (−2³¹ ÷ −1) also produce defined values without exceptions. For divide-by-zero the quotient is all ones and the remainder is the dividend; for overflow the quotient is the dividend and the remainder is 0.
  • Running 1,948 boundary pairs on QEMU, all matched a Python reference model. Without M, each multiply costs 86 extra instructions (a libgcc call), and each divide about 160 more.

The RV32I we saw in earlier parts has neither multiplication nor division. You can build multiplication from adds, shifts, and compares, but it is slow. From part 7 on, we add extensions one at a time. The first is the M extension, which adds integer multiply and divide. It has few instructions, but the defined results RISC-V produces in cases that "normally raise an exception," such as division by zero and overflow, carry straight into the design. This part checks the meaning of the instructions and the boundary rules on QEMU, and leaves the multiplier and divider circuits to part 19 (implementing the M extension).

The environment is the same as part 3: a bare-metal program on QEMU 11.1.3's virt board, printing results over UART. The machine was a Mac M1 Max and the compiler riscv64-elf-gcc 16.2.

1. The M Extension at a Glance

All M extension instructions are R format, distinguished from base integer operations by opcode and funct7. They reuse opcode OP (0110011), which RV32I's add and sub use, and set funct7 to 0000001. The three funct3 bits select the operation. The values below were taken directly from machine code produced by GNU as.

funct3 OP (0110011) OP-32 (0111011, RV64 only)
000 mul mulw
001 mulh
010 mulhsu
011 mulhu
100 div divw
101 divu divuw
110 rem remw
111 remu remuw

The top bit of funct3 separates multiply (0) from divide (1). For the decoder, it becomes "if funct7 = 0000001 it is M, and inst[14] picks the multiplier or the divider." RV64's W instructions use the OP-32 opcode like addw from part 5, and there are no W versions of the upper-half multiplies.

The specification explains why multiply and divide were left out of the base ISA: to save hardware in small implementations, or in applications where multiply and divide are rare or handed off to accelerators. So a core that implements only RV32I is a legitimate RISC-V core, and compilers for such cores turn multiplication into library calls. Section 6 measures that cost.

2. Multiplication: One Lower Half, Three Upper Halves

Multiplying two 32-bit numbers gives a 64-bit result. mul puts the lower 32 bits in rd. mulh, mulhsu, and mulhu put the upper 32 bits of the same product, differing in whether each operand is read as signed or unsigned.

Instruction rs1 read as rs2 read as Result
mul (doesn't matter) (doesn't matter) product [31:0]
mulh signed signed product [63:32]
mulhsu signed unsigned product [63:32]
mulhu unsigned unsigned product [63:32]

Why is there only one lower-half instruction? Let's follow the numbers. rs1 = 0xfffffff9, rs2 = 7. Read as signed, −7 × 7 = −49, which in 64 bits is 0xffffffff_ffffffcf. Read as unsigned, 4,294,967,289 × 7 = 30,064,771,023 = 0x00000006_ffffffcf. The lower 32 bits are 0xffffffcf in both, and only the upper 32 bits differ: 0xffffffff and 0x00000006.

Multiplying rs1=0xfffffff9 by rs2=7, the lower half is 0xffffffcf under all three interpretations; only the upper half differs: 0xffffffff for mulh and mulhsu, 0x00000006 for mulhu

It is the same reason two's complement addition does not need to know the sign. The lower n bits of an n-bit product are determined only by the two numbers modulo 2ⁿ, and the signed and unsigned interpretations differ by a multiple of 2ⁿ. The difference in the upper half can be written as follows. The unsigned value aua_u and the signed value asa_s are related by au=as+232a31a_u = a_s + 2^{32} a_{31}.

mulhu(a,b)=mulh(a,b)+a31⋅b+b31⋅a(mod232)\text{mulhu}(a, b) = \text{mulh}(a, b) + a_{31} \cdot b + b_{31} \cdot a \pmod{2^{32}}

In words, the upper half of the unsigned product is the upper half of the signed product plus "the other operand, added once more for each operand that was negative." In the example above, a31=1a_{31} = 1 and b31=0b_{31} = 0, so adding b = 7 to mulh's 0xffffffff (−1) gives 6. This matches QEMU's mulhu result 0x00000006. Multiplier hardware usually widens the operands by one bit (to 33 bits), chooses sign extension or zero extension, and handles all three instructions with the same signed multiplier. Part 19 covers this structure.

What is mulhsu for? As the specification explains, it is used when multiplying large signed multi-word numbers, to multiply the most significant word, which holds the sign bit, by the unsigned lower words. Here are a few boundary values (values from running on QEMU).

rs1 × rs2     mul         mulh        mulhu
-1 × -1       0x00000001  0x00000000  0xfffffffe
min × min     0x00000000  0x40000000  0x40000000
-7 × 7        0xffffffcf  0xffffffff  0x00000006

min is 0x80000000 (−2³¹). (−2³¹)² = 2⁶², so mulh is 0x40000000, and 2³¹ × 2³¹ read as unsigned is also 2⁶², so mulhu is the same. −1 × −1 is 1 as signed, so the upper half is 0, but read as unsigned it is (2³² − 1)², so the upper half is 0xfffffffe.

3. Division: Truncate Toward Zero, Remainder Takes the Dividend's Sign

div and divu produce the quotient, and rem and remu the remainder. Signed division truncates the quotient toward zero (discarding the fractional part). As a result, the remainder has the same sign as the dividend (the number being divided). This is the same rule as C's / and %.

Expression Quotient (div) Remainder (rem) Check
−7 ÷ 2 −3 (0xfffffffd) −1 (0xffffffff) −3 × 2 − 1 = −7
7 ÷ −2 −3 (0xfffffffd) 1 −3 × −2 + 1 = 7
−7 ÷ 2 (unsigned) 0x7ffffffc 1 divu, remu

The third row reads the same bits 0xfffffff9 as the unsigned 4,294,967,289. Same operands, yet the quotients are completely different: −3 and 2,147,483,644. So unlike the lower half of multiplication, division needs separate signed and unsigned versions.

4. Division by Zero and Overflow: Defined Values, No Exceptions

This is the most important rule in the M extension. Many ISAs trap on division by zero (x86's #DE). RISC-V does not. Instead, it defines the results.

Case Quotient div[u] Remainder rem[u]
x ÷ 0 All ones x
−2³¹ ÷ −1 (div/rem only) −2³¹ 0

These are the values from running on RV32 QEMU.

div  7 / 0          -> 0xffffffff
divu 7 / 0          -> 0xffffffff
rem  7 / 0          -> 0x00000007
remu 0xfffffff9 / 0 -> 0xfffffff9
div  0x80000000 / -1 -> 0x80000000
rem  0x80000000 / -1 -> 0x00000000
divu 0x80000000 / 0xffffffff -> 0

The divider just selects: if b=0, quotient 0xffffffff and remainder a; if a=−2³¹ and b=−1, quotient a and remainder 0; otherwise the truncated-toward-zero result. There is no trap path

These values were not picked arbitrarily. There are three reasons.

  • The identity holds. For divide-by-zero, quotient × 0 + remainder = x; for overflow, −2³¹ × −1 + 0 = 2³¹ ≡ −2³¹ (wrapping around in 32 bits). Dividend = divisor × quotient + remainder always holds within 32 bits.
  • It is the natural output of an unsigned divider. Restoring division does "if you can subtract, subtract and set the quotient bit to 1" at every step. If the divisor is 0, you can always subtract, so every quotient bit becomes 1 and the dividend is left as is in the remainder register. The specification says it defined signed division with the same value (all ones = −1) to make reusing the unsigned circuit easy.
  • There are no traps at all. To condense the specification's explanation: adding a divide-by-zero trap would make it the only arithmetic trap in the standard ISA. If a language requires an exception, a single beqz after the divide does it, and since that branch is almost never taken, the cost is small.

GCC did not insert a zero check for C's a / b (the divmod output in section 7 is just two lines, div and rem). That is because division by zero in C is undefined behavior. Languages that need a zero check (e.g., Java's ArithmeticException) have the compiler insert the branch itself.

5. RV64: W Instructions and the 128-Bit Product

In RV64M, the eight instructions such as mul and div operate on 64 bits, and there are separate 32-bit versions: mulw, divw, divuw, remw, and remuw. The rule is the same as for the W instructions in part 5. Compute using only the lower 32 bits, then sign-extend the 32-bit result to 64 bits. Whatever is in the upper 32 bits is ignored.

I checked with g = 0x12345678_7fffffff (garbage on top, 0x7fffffff below).

mulw  g * 7     -> 0x000000007ffffff9
divw  g / 0     -> 0xffffffffffffffff
remw  g / 0     -> 0x000000007fffffff
remuw 0x80000000 / 0  -> 0xffffffff80000000
divuw 0x80000000 / -1 -> 0x0000000000000000
divw  0x80000000 / -1 -> 0xffffffff80000000

The fourth line is the trap. Even though remuw is an "unsigned" remainder, the result 0x80000000 was sign-extended to 0xffffffff80000000. This is the part the specification nails down: "REMW and REMUW always sign-extend the 32-bit result to 64 bits, including on a divide by zero." The divide-by-zero quotient of divuw is also 0xffffffff sign-extended, so all 64 bits become 1. The principle from part 5, that RV64 registers carry 32-bit values in sign-extended form, applies to unsigned operations too.

GCC compiled C int multiplication and division on RV64 to a single mulw, divw, or remuw. A 128-bit product of two 64-bit values (unsigned __int128) became two instructions, mulhu + mul, with the same structure as the 64-bit product on RV32. I checked the boundary rules for 64-bit div and rem the same way: for −2⁶³ ÷ −1, the quotient was 0x8000000000000000 and the remainder 0.

6. Hands-On: QEMU Results vs. a Reference Model, and the Cost Without M

Correctness check. I ran the 8 RV32M instructions on all 81 pairs of 9 boundary values (0, 1, −1, 2, −2, 7, −7, 2³¹−1, −2³¹), and on RV64 the 13 instructions on 100 pairs of 10 values. Each instruction was inserted with inline assembly, and inputs were held in volatile variables so the compiler could not precompute them.

#define OP(op, a, b) ({ uint32_t r_; \
  __asm__ volatile(op " %0,%1,%2" \
    : "=r"(r_) : "r"(a), "r"(b)); r_; })
for (int i = 0; i < 9; i++)
  for (int j = 0; j < 9; j++) {
    volatile uint32_t a = V[i], b = V[j];
    show("mulh", a, b, OP("mulh", a, b));
    show("div", a, b, OP("div", a, b));
    /* … all 8 */
  }

I wrote the reference model directly with Python integers. Multiplication multiplies at infinite precision and then truncates; division takes abs(a) // abs(b), applies the sign, and handles divide-by-zero and overflow separately up front.

def div(a, b):
    a, b = sx(a, n), sx(b, n)
    if b == 0: return M          # all ones
    if a == -(1 << (n-1)) and b == -1:
        return a & M             # quotient = dividend
    q = abs(a) // abs(b)         # truncate toward zero
    return (-q if (a < 0) != (b < 0) else q) & M

All 648 RV32M cases (8 × 81 pairs) and 1,300 RV64M cases (13 × 100 pairs) matched the reference model, and the 13 encodings in the section 1 table also matched GNU as output.

How much does it cost without M? I compiled a loop doing multiplies and divides on 1,000 random pairs (fixed-seed LCG, a over the full 32 bits, b in 1~65,535) with three -march settings, launched QEMU with -icount shift=0, and counted with minstret (instructions executed). Optimization was -O2; the builds without M call libgcc's __mulsi3, __divsi3, and so on. All three builds produced the same computed result (checksum).

Averaged over 1,000 iterations, rv32i uses 93 instructions per iteration for a*b, 165 for a/b, and 340 for quotient and remainder, while rv32im uses 7~9. Zmmul matches rv32im for multiplication only

Operation (per iteration) rv32i rv32i_zmmul rv32im
a ^ b (baseline) 7.0 7.0 7.0
a * b 93.2 7.0 7.0
64-bit product 207.3 9.0 9.0
a / b (signed) 165.2 165.2 7.0
a / b, a % b 339.6 339.6 9.0

Subtracting the baseline loop (xor), each multiply costs 86.2 extra instructions, each signed divide 158.2, and computing both quotient and remainder 332.6. With hardware multiply, a single mul takes the place of the xor, so the instruction count does not grow. These are instruction counts, not cycle counts. How many cycles mul and div take on a real core depends on the implementation, and division in particular commonly takes many cycles.

7. Instruction Pairing Order and Zmmul

When you need both the high and low parts. If you need the full 64-bit product (128-bit on RV64), the specification recommends this order.

mulh[[s]u] rdh, rs1, rs2
mul        rdl, rs1, rs2
# same source order, and rdh must differ from rs1 and rs2
div[u]     rdq, rs1, rs2
rem[u]     rdr, rs1, rs2
# rdq must differ from rs1 and rs2

Written this way, the microarchitecture can merge the two instructions into a single multiply (or a single divide) (macro-op fusion). The reason for the conditions is simple. If the first instruction overwrites a source register, the second instruction's inputs change, and it is no longer the same product.

GCC 16.2's actual output got it half right.

umul64:                 divmod:
  mv    a5,a1             div  a5,a0,a1
  mulhu a1,a0,a1          rem  a0,a0,a1
  mul   a0,a0,a5          sw   a5,0(a2)
  ret                     sw   a0,0(a3)

divmod is exactly the recommended form (rdq=a5 differs from the sources, and the source order is the same). In umul64, by contrast, mulhu wrote its result to a1, the same as rs2. So mul had to use the copy in a5, and the two instructions' sources became (a0, a1) and (a0, a5), which does not meet the fusion condition. It seems to be because the calling convention requires the 64-bit result to be returned in a1:a0 (an estimate). It may differ if the function is inlined and the register constraints are relaxed.

GCC also uses mulhu for division by a constant. a / 10 (unsigned) became the magic constant 0xcccccccd with mulhu and srli 3 instead of div, because in many implementations an upper-half multiply is much faster than a divide. For a / 8 (signed), to truncate negative values toward zero, it added 7 using srai, andi, and add, then did srai 3.

Zmmul. This extension takes only the multiply instructions from M (mul mulh mulhsu mulhu, plus mulw on RV64). The encodings are the same as M, and M includes Zmmul. The specification gives examples such as microcontrollers, where division is rare and divider area is not worth it, and FPGA soft cores, where a multiplier exists as a hard block but a divider would have to be built from logic. The rv32i_zmmul column in the section 6 table shows exactly this: multiplication matches rv32im, and division calls __divsi3 and __modsi3 like rv32i.

8. Design Checklist: The M Extension

Item Rule
Decode opcode OP/OP-32, funct7 = 0000001. inst[14] selects multiply (0) or divide (1)
The three mulh variants Sign- or zero-extend operands to 33 bits and use one multiplier. mulhsu signs rs1 only
Divide by zero Quotient all ones, remainder = dividend. Same for div and divu. No exception
Overflow div: −2^(XLEN−1) ÷ −1 → quotient = dividend, remainder 0. No exception
Remainder sign If nonzero, the dividend's sign. The quotient truncates toward zero
W instructions Use only the lower 32 bits and sign-extend the 32-bit result. Including remuw and divuw
W boundary values Decide with L = 32: divw overflows when the lower 32 bits are 0x80000000 and 0xffffffff
Trap path M instructions raise no exceptions other than illegal instruction. A result selector is all you need
Multi-cycle If division takes multiple cycles, stall instructions that depend on rd until the result is written
Fusion (optional) Whether to fuse mulh+mul and div+rem is up to the implementation. Check the source and rd conditions
Zmmul only Unimplemented divide encodings normally raise an illegal instruction exception

The "trap path" row matters a lot for the design. Since no arithmetic exception comes out of anywhere in the integer execution units, you can split the multiplier and divider off as separate multi-cycle units without worrying about precise exceptions (part 27). The result simply arrives late; there is no case where "a trap happens midway and you have to roll back." The divide-by-zero check is also sometimes used for early termination: look at whether the divisor is 0 before starting the divide, and select the result right away. The specification defines only the result value; how many cycles it takes is up to the implementation.

Summary

  • The M extension has 4 multiplies, 4 divides, and 5 W instructions on RV64. The lower half of a product is sign-independent, so there is one mul; the upper half has three: mulh, mulhsu, and mulhu.
  • Signed division truncates toward zero, and the remainder follows the dividend's sign. Divide-by-zero (quotient all ones, remainder the dividend) and overflow (quotient the dividend, remainder 0) are defined values without exceptions.
  • The W instructions sign-extend their results, even the unsigned remuw and divuw. All 1,948 QEMU runs matched the reference model.
  • Without M, each multiply costs 86 extra instructions and each divide about 160. The mulh+mul and div+rem orders are the forms recommended for fusion, and GCC produced that form only for division.

Next post: Part 8 looks at the A extension. It covers the atomic instructions needed when several harts work on the same memory, the reservation and failure conditions of lr.w and sc.w, the nine AMOs such as amoswap and amoadd, and the meaning of the aq and rl bits. I will write a spinlock and an atomic counter both ways and make several harts contend on QEMU.

References

  • The RISC-V Instruction Set Manual, Volume I: Unprivileged Architecture, Version 20240411. Chapter 13 "M" Extension (13.1 Multiplication, 13.2 Division and the divide-by-zero/overflow table, 13.3 Zmmul), Chapter 4 RV64I (W instructions). https://github.com/riscv/riscv-isa-manual
  • D. Patterson, A. Waterman, The RISC-V Reader: An Open Architecture Atlas, 2017. Chapter 4 (RV32M)
  • D. Harris, S. Harris, Digital Design and Computer Architecture: RISC-V Edition, 2021. 5.2.6 Multiplication, 5.2.7 Division
  • GCC libgcc integer arithmetic routines (__mulsi3, __divsi3, __modsi3). https://gcc.gnu.org/onlinedocs/gccint/Integer-library-routines.html
  • QEMU -icount option and virt board documentation. https://www.qemu.org/docs/master/system/riscv/virt.html

Similar posts