Skip to content
Published at:

14. M 扩展:乘除法指令

第 5 章提到 RISC-V 基础整数指令集 RV64I 不包含乘除法——add/sub 可以解决一切算术的错觉来自补码,但它解决不了"两个 64 位数相乘得 128 位结果"这类需求。M 扩展(Integer Multiplication and Division)填补这个空白,提供硬件整数乘除。本章详述 M 扩展的所有指令、编码、边界行为和编译器优化。

M 扩展概述

为什么乘除是"扩展"而非"基础"

RISC-V 基础 ISA 刻意将乘除法排除在 RV32I/RV64I 之外,原因有三:

  1. 硬件成本:32 位乘法器需要约 2000 个逻辑门,除法器更多且慢——在面积敏感的低端 MCU 上这笔开销不值得
  2. 使用频率:大量嵌入式控制逻辑(传感器读取、GPIO 操作、状态机转移)根本不需要乘除
  3. 软件可替代:乘法可用循环移位加模拟(Booth 算法精神),除法可用试商减法模拟——慢但可用

因此,RV64I + M 是常见组合;没有 M 扩展时,编译器调用 __muldi3/__divdi3 等软浮点库函数。

编码空间

M 扩展指令复用基础整数 R-type 的 opcode 空间:

属性
opcode0110011(与 add/sub 相同)
funct70000001(bit 30 = 0,区别于 sub/sra 的 funct7=0100000)

funct3 区分 8 种操作(乘法 4 种 + 除法 4 种),funct7 统一为 0000001。这条设计线索说明:M 扩展的指令"看起来"是 R-type 整数操作的一个子类——解码器只需多检查一层 funct7 的第 0 位是否为 1。

指令总览

指令funct3操作描述
mul000rd = (rs1 * rs2)[63:0],低 64 位
mulh001有符号乘高 64 位
mulhsu010有符号(rs1) × 无符号(rs2) 高 64 位
mulhu011无符号乘高 64 位
div100有符号除,向零取整
divu101无符号除
rem110有符号余数
remu111无符号余数

RV64 额外提供 mulw/divw/divuw/remw/remuw 五条 32 位变体(opcode=0111011,funct7=0000001),操作数截取低 32 位,结果符号扩展到 64 位。

乘法指令

基本形式

asm
mul    rd, rs1, rs2    # rd = (rs1 * rs2)[63:0]    低 64 位,有符号与无符号结果相同
mulh   rd, rs1, rs2    # rd = (rs1 * rs2)[127:64]  有符号 × 有符号的高 64 位
mulhu  rd, rs1, rs2    # rd = (rs1 * rs2)[127:64]  无符号 × 无符号的高 64 位
mulhsu rd, rs1, rs2    # rd = (rs1 * rs2)[127:64]  rs1 有符号,rs2 无符号

这四条指令覆盖了任意 64 位乘法语义的所有组合。RISC-V 没有单独的 "有符号 128 位结果" 或 "无符号 128 位结果" 指令——而是通过组合 mul(低 64 位)+ mulh/mulhu/mulhsu(高 64 位)来覆盖。低 64 位乘法结果与符号无关(补码的算术性质保证),只需一条 mul

128 位完整乘法

asm
# 计算 a0 * a1,128 位结果存入 t1:t0(t0=低64,t1=高64)
# ---- 有符号版本 ----
mul   t0, a0, a1        # 低 64 位(有符号、无符号结果相同)
mulh  t1, a0, a1        # 高 64 位(有符号)

# ---- 无符号版本 ----
mul   t0, a0, a1        # 低 64 位
mulhu t1, a0, a1        # 高 64 位(无符号)

汇编器建议先写 mulh 再写 mul(先后顺序),因为某些实现可以对相邻的 mulh+mul 对做宏融合——在一条流水线中同时产出高、低 64 位。

32 位乘法(RV64M)

asm
mulw rd, rs1, rs2       # rd = sign_ext((rs1[31:0] * rs2[31:0])[31:0])

mulw 将 rs1 和 rs2 的低 32 位相乘,取结果的低 32 位,符号扩展到 64 位后写入 rd。编译器对 C 语言的 int32_t * int32_t 产生 mulw——语义正确且节省对高 32 位的无关计算。

常数乘法优化

编译器极少对字面量常数直接生成 mul——移位+加法的组合体效率远高于通用乘法器。以下是常见常数乘法的等价变换:

asm
# t1 = t0 * 3
slli t1, t0, 1          # t1 = t0 * 2
add  t1, t1, t0          # t1 = t0 * 2 + t0 = t0 * 3

# t1 = t0 * 5
slli t1, t0, 2           # t1 = t0 * 4
add  t1, t1, t0          # t1 = t0 * 4 + t0 = t0 * 5

# t1 = t0 * 10
slli t1, t0, 3           # t1 = t0 * 8
slli t2, t0, 1           # t2 = t0 * 2
add  t1, t1, t2          # t1 = t0 * 8 + t0 * 2 = t0 * 10

# t1 = t0 * 12
slli t1, t0, 3           # t1 = t0 * 8
slli t2, t0, 2           # t2 = t0 * 4
add  t1, t1, t2          # t1 = t0 * 8 + t0 * 4 = t0 * 12

RISC-V 没有像 x86 的 lea 那样能在一个周期内完成"移位+加法"的地址生成指令——上述序列需 2-3 条指令,但每条都是单周期 ALU 操作。当乘数是 2 的幂方、或可分解为少量 2 的幂之和时,移位加法优于 mul(无 M 扩展时更是唯一选择)。

RV32 中的 64 位乘法

RV32M 上计算 64 位乘法的需求催生了 mulh/mulhu/mulhsu 的原始设计动机——只有 32 位寄存器,却需要 64 位结果。以下是 RV32M 上实现 64 位无符号乘法的完整序列:

asm
# RV32M: result[63:0] = a[31:0] * b[31:0]
# a_lo: a0, a_hi: a1; b_lo: a2, b_hi: a3
# result_lo: t0, result_hi: t1
mul   t0, a0, a2          # t0 = a_lo * b_lo (低 32 位)
mulhu t1, a0, a2          # t1 = a_lo * b_lo (高 32 位)
mul   t2, a1, a2          # t2 = a_hi * b_lo
add   t1, t1, t2          # t1 += a_hi * b_lo
mul   t2, a0, a3          # t2 = a_lo * b_hi
add   t1, t1, t2          # t1 += a_lo * b_hi

这段代码精确反映了小学乘法的竖式展开——四个部分积相加。在 RV64 中同样可用 mulw 系列实现 128 位乘法,原理一致。

除法指令

基本形式

asm
div  rd, rs1, rs2    # rd = rs1 / rs2     有符号除,向零取整
divu rd, rs1, rs2    # rd = rs1 / rs2     无符号除
rem  rd, rs1, rs2    # rd = rs1 % rs2     有符号余数
remu rd, rs1, rs2    # rd = rs1 % rs2     无符号余数

RISC-V 的除法遵循 C99 语义:商向零取整,余数满足 dividend = quotient * divisor + remainder,且余数的符号与被除数相同。

asm
# 示例:有符号除法的商与余数
li   t0, 10
li   t1, 3
div  t2, t0, t1         # t2 = 10 / 3 = 3(向零取整,不是 3.33)
rem  t3, t0, t1         # t3 = 10 % 3 = 1(10 = 3 * 3 + 1)

li   t0, -10
li   t1, 3
div  t2, t0, t1         # t2 = -10 / 3 = -3(向零取整,不是 -4)
rem  t3, t0, t1         # t3 = -10 % 3 = -1(-10 = 3 * -3 + (-1),余数与被除数同号)

除零行为——RISC-V 的特殊选择

RISC-V 在除零问题上的设计与其他架构截然不同:不抛异常,返回特定值

asm
# 除零的返回值
li   t0, 42

# 除法除零
div  t1, t0, x0         # t1 = -1(全 1 位模式,即 0xFFFFFFFFFFFFFFFF)
divu t1, t0, x0         # t1 = 2^64 - 1(MAX_UINT,同样是全 1)

# 余数除零
rem  t1, t0, x0         # t1 = t0(原样返回被除数)
remu t1, t0, x0         # t1 = t0(原样返回被除数)

这个设计选择的核心原因:

  1. 无异常开销:OS 无需处理除零陷阱,控制流不被打断——在嵌入式实时场景中至关重要
  2. 可预测性:除法永远不会因输入值而导致程序崩溃
  3. 软件检测:代码可以在除法后立即检查结果是否全 1,自行决定后续策略:
asm
div  t1, t0, t1           # 执行除法
not  t2, t1               # t2 = ~t1
beqz t2, handle_divzero   # 如果 t1 全 1(~t1 == 0),说明发生了除零

溢出:唯一的情况

RISC-V 除法有一个且仅有一个溢出场景:-2^63 / -1

asm
li   t0, -9223372036854775808    # -2^63(0x8000000000000000)
li   t1, -1
div  t2, t0, t1                  # 溢出!正确结果 2^63 超出了 64 位有符号范围

规范规定此场景下 div 返回被除数(即 -2^63),rem 返回 0。实际代码中此场景极罕见,但编写通用库代码时应考虑。

除以常数优化——强度削减

编译器从不直接对常数做除法——以下面的 x / 10 为例,编译器会生成类似这样的序列:

asm
# 等效于 t0 = rs1 / 10(无符号)
# 核心思路:除以 10 = 乘以 (2^N / 10),再右移 N 位
# 编译器预先计算 magic number 和 shift amount
li    t1, 0xCCCCCCCCCCCCCCCD     # 预计算的魔法数(2^67 / 10 的近似)
mulhu t1, t0, t1                 # 高 64 位乘法
srli  t1, t1, 3                  # t1 = t1 >> 3

这个技术称为"除法的强度削减"——将昂贵的除法替换为廉价的乘法+移位。mulhu 将 128 位乘积的高 64 位捕获为"除以 2^64"的近似,再配合右移完成精确除法。有符号除法的强度削减更复杂(需要处理负数向零取整),但原理相同:一次 mulh/mulhu + 若干移位和加法替换一次 div

32 位除法(RV64M)

asm
divw  rd, rs1, rs2      # 64 位结果 = sign_ext(32 位 rs1 / 32 位 rs2)
divuw rd, rs1, rs2      # 无符号版
remw  rd, rs1, rs2      # 余数版
remuw rd, rs1, rs2      # 无符号余数版

这些指令对应 C 语言中 int32_t / int32_t 的精确语义。

性能模型

典型延迟

指令典型周期数依赖因素
mul3-5乘法器宽度、流水线级数
mulh/mulhu3-5与 mul 相同(共享乘法器阵列)
div/divu20-80实现算法(SRT / Newton 迭代)、操作数位宽
rem/remu20-80与除法相同(除法器同时产出商和余数)

RISC-V 规范不保证执行周期数——上述数据基于常见开源核(Rocket、BOOM、C906)的实测。关键是除法的延迟比乘法大一个数量级,这是所有 ISA 的共性,来自除法算法的内在串行性。

M 扩展与编译器最佳实践

  1. 循环中的除法是性能杀手:将 x / d 提到循环外,或改用乘法倒数(如果 d 是常数)
  2. 取模 2 的幂andi 而非 remx % 8 = andi x, 7
  3. 除以 2 的幂srli/srai 而非 divx / 8 = srli x, 3

这些优化的效果不只体现在 M 扩展上——它们让代码在任何情况下都更快,在没有 M 扩展时更是从"不可能"变为"可能"。

本章要点

  • M 扩展复用基础 R-type 的 opcode=0110011,以 funct7=0000001 区分;8 条指令覆盖乘法(funct3=000..011)和除法(funct3=100..111)
  • 128 位完整乘法用 mul(低 64 位)+ mulh/mulhu(高 64 位)组合;常数乘除编译期优化为移位+加法
  • RISC-V 除零不抛异常:div 返 -1、divu 返 MAX、rem 返被除数——这是实时系统友好的设计选择
  • 除法延迟是乘法的 5-20 倍;取模/除 2 的幂用位运算替代是编写高效汇编的基本素养
  • 没有 M 扩展时,编译器调用 __muldi3/__divdi3 软浮点库——硬件 M 扩展的存在对性能有量级影响