16. F 与 D 扩展:浮点指令
前 15 章的所有计算都在整数域——寄存器存的是 64 位整数,add/mul 操作的是整数 ALU。但科学计算、图形渲染、信号处理等领域离不开 IEEE 754 浮点数。RISC-V 将浮点支持分层为 F 扩展(单精度,32 位)和 D 扩展(双精度,64 位,依赖 F),引入独立的浮点寄存器文件和完整的 IEEE 754 操作集。本章详述浮点寄存器架构、全部指令族、以及 IEEE 754 在汇编层面的体现。
浮点扩展概述
F 与 D 的关系
- F 扩展:32 个 32 位浮点寄存器 f0-f31,提供单精度 load/store、算术、比较、转换指令
- D 扩展:将浮点寄存器扩展为 64 位宽,新增双精度 load/store、算术、单/双精度互转指令。D 依赖 F——无 F 不能有 D
- Q 扩展(四精度,128 位)和 Zfh 扩展(半精度,16 位)同样是独立扩展
汇编助记符中以 .s 结尾的是单精度操作,以 .d 结尾的是双精度操作。在 D 扩展存在时,浮点寄存器实际为 64 位宽——存单精度值时只用低 32 位,高 32 位保持为 NaN-boxing(全 1)。
独立的寄存器文件
浮点寄存器 f0-f31 与整数寄存器 x0-x31 是完全独立的寄存器文件——这在 RISC-V 设计中是唯一的多寄存器文件场景。两者之间无法直接搬移数据,必须通过以下途径:
- 内存中转:整数 store → 浮点 load(或反之)
- fcvt 转换指令:直接在同一操作中搬移位模式并转换格式
- fmov(RV64D 提供):直接复制位模式(不转换)
这也带来一个性能微架构提示:整数和浮点寄存器文件之间的数据传输通常需跨越执行单元的端口,延迟比寄存器文件内部移动大 1-2 个周期。
浮点寄存器 ABI
命名与用途
| 寄存器 | ABI 名称 | 分类 | 用途 |
|---|---|---|---|
| f0-f7 | ft0-ft7 | caller-saved | 浮点临时寄存器 |
| f8-f9 | fs0-fs1 | callee-saved | 浮点被保存寄存器 |
| f10-f11 | fa0-fa1 | caller-saved | 浮点参数/返回值 |
| f12-f17 | fa2-fa7 | caller-saved | 浮点参数 3-7 |
| f18-f27 | fs2-fs11 | callee-saved | 浮点被保存寄存器 |
| f28-f31 | ft8-ft11 | caller-saved | 临时寄存器 |
与整数 ABI 的结构镜像——参数寄存器(fa0-fa7)对齐 a0-a7,临时寄存器(ft)对齐 t 系列。注意浮点被保存寄存器 fs0-fs11 共 12 个,比整数 s0-s11 多 1 个(整数的 s0/fp 被 x8 占用,浮点的 fs0 从 f8 开始)。
fcsr:浮点控制状态寄存器
fcsr 汇集浮点运算的舍入模式和异常标志——它不是通过常规读写在寄存器文件中访问,而是通过专门的 CSR 指令:
frcsr t0 # 读 fcsr 到整数寄存器
fscsr t0, t1 # 写 t1 到 fcsr,旧值存入 t0
fscsr t1 # fscsr x0, t1 —— 写 fcsr,丢弃旧值fcsr 的位域结构:
| 位 | 字段 | 说明 |
|---|---|---|
| 7:5 | frm | 舍入模式:000=RNE, 001=RTZ, 010=RDN, 011=RUP, 100=RMM |
| 4 | NV | Invalid Operation 异常标志 |
| 3 | DZ | Divide by Zero 异常标志 |
| 2 | OF | Overflow 异常标志 |
| 1 | UF | Underflow 异常标志 |
| 0 | NX | Inexact 异常标志 |
大多数浮点指令自带 3 位 rm(舍入模式)字段,若其值为 111 则使用 fcsr 中的 frm 域;否则使用指令指定的舍入模式。这条设计让指令可以在需要的时刻精确控制舍入——称为"动态舍入",是 RISC-V 浮点设计的独到之处。
浮点 Load/Store
# 单精度
flw rd, offset(rs1) # rd = sign_ext(MEM[rs1 + offset][31:0]),加载到 f 寄存器
fsw rs2, offset(rs1) # MEM[rs1 + offset] = rs2[31:0]
# 双精度
fld rd, offset(rs1) # rd = MEM[rs1 + offset](64 位)
fsd rs2, offset(rs1) # MEM[rs1 + offset] = rs2语法与整数 lw/sw/ld/sd 完全一致——寻址方式(基址 + 12 位有符号偏移)、宽度(32 或 64 位)、对齐要求都同模同构。唯一区别是目标/源在 f 寄存器文件而非 x 寄存器文件。
# 示例:从栈加载和存储浮点值
fld fa0, 0(sp) # fa0 = *(double*)sp
fsd fs0, 8(sp) # *(double*)(sp+8) = fs0opcode:浮点 load 为 0000111,浮点 store 为 0100111;funct3=010 为单精度,funct3=011 为双精度。
浮点运算指令
基本算术
# 单精度(.s)和双精度(.d)形式
fadd.s rd, rs1, rs2 # rd = rs1 + rs2
fadd.d rd, rs1, rs2
fsub.s rd, rs1, rs2 # rd = rs1 - rs2
fsub.d rd, rs1, rs2
fmul.s rd, rs1, rs2 # rd = rs1 * rs2
fmul.d rd, rs1, rs2
fdiv.s rd, rs1, rs2 # rd = rs1 / rs2
fdiv.d rd, rs1, rs2
fsqrt.s rd, rs1 # rd = sqrt(rs1)(rs2 不使用)
fsqrt.d rd, rs1
# 最小值/最大值
fmin.s rd, rs1, rs2 # rd = min(rs1, rs2)
fmax.s rd, rs1, rs2 # rd = max(rs1, rs2)
fmin.d rd, rs1, rs2
fmax.d rd, rs1, rs2R-type,opcode=1010011。精度由指令格式字段(bits 26:25 = funct2)指示:00=单精度(.s)、01=双精度(.d)。操作由 funct5(bits 31:27)指示:00000=fadd、00001=fsub、00010=fmul、00011=fdiv、01011=fsqrt、00101=fmin/fmax(funct3=000 为 min,funct3=001 为 max)。
符号注入指令
fsgnj.s rd, rs1, rs2 # rd = {rs2[31], rs1[30:0]}(注入符号)
fsgnjn.s rd, rs1, rs2 # rd = {~rs2[31], rs1[30:0]}(注入反符号)
fsgnjx.s rd, rs1, rs2 # rd = {rs1[31] ^ rs2[31], rs1[30:0]}(异或符号)
fsgnj.d rd, rs1, rs2
fsgnjn.d rd, rs1, rs2
fsgnjx.d rd, rs1, rs2这些指令不执行算术操作——只修改符号位。它们是 fabs(取绝对值,fsgnj.s rd, rs1, rs1 并在 rs1 的符号位清零)、fneg(取负,fsgnjn.s rd, rs1, rs1)的底层硬件实现。RISC-V 没有独立的 fabs/fneg 指令,汇编器将这些伪指令展开为 fsgnj/fsgnjn。
融合乘加(FMA)
FMA 是浮点计算中的重型操作——在一条指令中计算 a * b + c,且只在最后舍入一次(而非先舍入乘法的结果再舍入加法)。RISC-V 提供四种变体:
# 格式: op rd, rs1, rs2, rs3 —— 四个寄存器操作数!
fmadd.s rd, rs1, rs2, rs3 # rd = (rs1 * rs2) + rs3
fmsub.s rd, rs1, rs2, rs3 # rd = (rs1 * rs2) - rs3
fnmsub.s rd, rs1, rs2, rs3 # rd = -(rs1 * rs2) + rs3
fnmadd.s rd, rs1, rs2, rs3 # rd = -(rs1 * rs2) - rs3
fmadd.d rd, rs1, rs2, rs3
fmsub.d rd, rs1, rs2, rs3
fnmsub.d rd, rs1, rs2, rs3
fnmadd.d rd, rs1, rs2, rs3FMA 使用独立的 R4-type 格式(4 个寄存器 + 2 位格式指示),opcode 分别为 1000011(fmadd)、1000111(fmsub)、1001011(fnmsub)、1001111(fnmadd)。这是 RISC-V 中唯一的四寄存器指令格式——三个源寄存器(rs1, rs2, rs3)加一个目标寄存器(rd)。
# FMA 使用示例:双线性插值 a*(1-t) + b*t
# 设 fa0=a, fa1=b, fa2=t, fa3=1.0
fmsub.d ft0, fa0, fa2, fa0 # ft0 = a*t - a = a*(t-1)
fmsub.d ft1, fa1, fa2, ft0 # ft1 = b*t - a*(t-1) = a*(1-t) + b*tFMA 在数值精度上优于分开的乘法+加法——单次舍入减少了累积误差。在高性能数值计算(矩阵乘、卷积、多项式求值)中,编译器会积极将 a*b + c 模式转化为 FMA。
转换指令
浮点与整数之间的转换、不同精度之间的互转,在 RISC-V 中通过统一的 fcvt 指令族完成:
# 浮点 → 整数
fcvt.w.s rd, rs1 # rd = (int32_t)rs1 有符号,单精度
fcvt.wu.s rd, rs1 # 无符号版本
fcvt.l.s rd, rs1 # rd = (int64_t)rs1 有符号,单精度
fcvt.lu.s rd, rs1 # 无符号版本
fcvt.w.d rd, rs1 # rd = (int32_t)rs1 有符号,双精度
fcvt.wu.d rd, rs1
fcvt.l.d rd, rs1 # rd = (int64_t)rs1 有符号,双精度
fcvt.lu.d rd, rs1
# 整数 → 浮点
fcvt.s.w rd, rs1 # rd = (float)rs1 有符号,结果单精度
fcvt.s.wu rd, rs1 # 无符号版本
fcvt.s.l rd, rs1 # 64 位整数 → 单精度
fcvt.s.lu rd, rs1
fcvt.d.w rd, rs1 # rd = (double)rs1 有符号,结果双精度
fcvt.d.wu rd, rs1
fcvt.d.l rd, rs1 # 64 位整数 → 双精度
fcvt.d.lu rd, rs1
# 浮点精度互转
fcvt.s.d rd, rs1 # rd = (float)(double)rs1 双→单
fcvt.d.s rd, rs1 # rd = (double)(float)rs1 单→双转换指令的 R-type 编码中,rs2 字段指示源格式(00000=.s, 00001=.d,对于整数转换,rs2 指示有/无符号),而 funct7 的高位指示方向和目标格式。大值转换超出目标范围时触发 NV 异常标志,并返回适当值(最大/最小/无穷大)。
# 示例:华氏温度转摄氏温度
# fa0 = 华氏温度(双精度)
fld fa1, const_32, t0 # fa1 = 32.0
fld fa2, const_5over9, t0 # fa2 = 5.0 / 9.0
fmul.d fa0, fa0, fa2 # f * (5/9) → 注意这里是伪代码,演示转换思路
fsub.d fa0, fa0, fa1浮点比较
浮点比较指令结果写入整数寄存器(不是浮点寄存器)——0 或 1:
feq.s rd, rs1, rs2 # rd = (rs1 == rs2) ? 1 : 0
feq.d rd, rs1, rs2
flt.s rd, rs1, rs2 # rd = (rs1 < rs2) ? 1 : 0
flt.d rd, rs1, rs2
fle.s rd, rs1, rs2 # rd = (rs1 <= rs2) ? 1 : 0
fle.d rd, rs1, rs2R-type,opcode=1010011;funct3=010 为 feq,funct3=001 为 flt,funct3=000 为 fle。结果通过整数寄存器传递——这让你能用普通的 beq/bne/beqz/bnez 对浮点比较结果做分支:
# if (fa0 < fa1) goto loop
flt.d t0, fa0, fa1
bnez t0, loop与整数的 slt 逻辑一致——不设标志位,结果写寄存器,再配合分支指令做条件跳转。RISC-V 从始至终没有条件码寄存器。
NaN 行为
IEEE 754 规定任何与 NaN 的比较都应返回 false(无序比较)。RISC-V 严格遵循:
# 设 fa0 = NaN, fa1 = 5.0
feq.s t0, fa0, fa1 # t0 = 0(NaN 不等于任何值,包括 NaN 自身)
flt.s t0, fa0, fa1 # t0 = 0(NaN < 5.0 → false)
fle.s t0, fa0, fa1 # t0 = 0(NaN <= 5.0 → false)
feq.s t0, fa0, fa0 # t0 = 0(NaN != NaN)要检测 NaN,使用 fclass 指令或等价的比较——feq.s t0, fa0, fa0,t0=0 说明源是 NaN。
完整示例:平方根逼近(Newton 法)
# 用 Newton 法逼近 sqrt(x) —— 双精度
# fa0 = x, 返回 fa0 = sqrt(x)
# y_{n+1} = (y_n + x / y_n) / 2
sqrt_newton:
fld ft0, two, t0 # ft0 = 2.0
fdiv.d ft1, fa0, ft0 # 初始猜测: y = x / 2
sqrt_loop:
fmv.d ft2, ft1 # 保存当前 y
fdiv.d ft3, fa0, ft1 # ft3 = x / y
fadd.d ft1, ft1, ft3 # ft1 = y + x/y
fdiv.d ft1, ft1, ft0 # ft1 = (y + x/y) / 2
fsub.d ft4, ft1, ft2 # 变化 = new_y - old_y
fabs.d ft4, ft4 # abs(变化)
fld ft5, epsilon, t0 # ft5 = 收敛阈值
flt.d t1, ft5, ft4 # abs(变化) > epsilon ?
bnez t1, sqrt_loop # 未收敛,继续迭代
fmv.d fa0, ft1 # 返回值
ret
.data
two: .double 2.0
epsilon: .double 1e-12实际生产代码中,硬件 fsqrt.d 比 Newton 迭代快数十倍——这个例子意在展示浮点指令序列在数值算法中的用法。
本章要点
- F 扩展提供 32 个 32 位浮点寄存器,D 扩展扩展为 64 位并新增双精度操作;f0-f31 与 x0-x31 独立,数据移动需经 load/store 或转换指令
- fcsr 控制舍入模式(5 种)和异常标志(5 种);动态舍入(rm=111 时使用 fcsr 的 frm)为每指令精确控制提供硬件支持
- 浮点算术(fadd/fsub/fmul/fdiv/fsqrt/fmin/fmax)和符号注入(fsgnj/fsgnjn/fsgnjx)使用 opcode=
1010011,精度由指令格式位指示 - 融加乘(FMA)以四寄存器格式(R4-type)在一条指令内完成 a*b+c 且单次舍入——数值算法的精度和性能基石
- 浮点比较结果写入整数寄存器(0/1),配合 bne/beq 做分支;NaN 上所有比较返回 false——严格遵循 IEEE 754