Skip to content
Published at:

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-f7ft0-ft7caller-saved浮点临时寄存器
f8-f9fs0-fs1callee-saved浮点被保存寄存器
f10-f11fa0-fa1caller-saved浮点参数/返回值
f12-f17fa2-fa7caller-saved浮点参数 3-7
f18-f27fs2-fs11callee-saved浮点被保存寄存器
f28-f31ft8-ft11caller-saved临时寄存器

与整数 ABI 的结构镜像——参数寄存器(fa0-fa7)对齐 a0-a7,临时寄存器(ft)对齐 t 系列。注意浮点被保存寄存器 fs0-fs11 共 12 个,比整数 s0-s11 多 1 个(整数的 s0/fp 被 x8 占用,浮点的 fs0 从 f8 开始)。

fcsr:浮点控制状态寄存器

fcsr 汇集浮点运算的舍入模式和异常标志——它不是通过常规读写在寄存器文件中访问,而是通过专门的 CSR 指令:

asm
frcsr t0                # 读 fcsr 到整数寄存器
fscsr t0, t1            # 写 t1 到 fcsr,旧值存入 t0
fscsr t1                # fscsr x0, t1 —— 写 fcsr,丢弃旧值

fcsr 的位域结构:

字段说明
7:5frm舍入模式:000=RNE, 001=RTZ, 010=RDN, 011=RUP, 100=RMM
4NVInvalid Operation 异常标志
3DZDivide by Zero 异常标志
2OFOverflow 异常标志
1UFUnderflow 异常标志
0NXInexact 异常标志

大多数浮点指令自带 3 位 rm(舍入模式)字段,若其值为 111 则使用 fcsr 中的 frm 域;否则使用指令指定的舍入模式。这条设计让指令可以在需要的时刻精确控制舍入——称为"动态舍入",是 RISC-V 浮点设计的独到之处。

浮点 Load/Store

asm
# 单精度
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 寄存器文件。

asm
# 示例:从栈加载和存储浮点值
fld  fa0, 0(sp)          # fa0 = *(double*)sp
fsd  fs0, 8(sp)          # *(double*)(sp+8) = fs0

opcode:浮点 load 为 0000111,浮点 store 为 0100111;funct3=010 为单精度,funct3=011 为双精度。

浮点运算指令

基本算术

asm
# 单精度(.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, rs2

R-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)。

符号注入指令

asm
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 提供四种变体:

asm
# 格式: 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, rs3

FMA 使用独立的 R4-type 格式(4 个寄存器 + 2 位格式指示),opcode 分别为 1000011(fmadd)、1000111(fmsub)、1001011(fnmsub)、1001111(fnmadd)。这是 RISC-V 中唯一的四寄存器指令格式——三个源寄存器(rs1, rs2, rs3)加一个目标寄存器(rd)。

asm
# 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*t

FMA 在数值精度上优于分开的乘法+加法——单次舍入减少了累积误差。在高性能数值计算(矩阵乘、卷积、多项式求值)中,编译器会积极将 a*b + c 模式转化为 FMA。

转换指令

浮点与整数之间的转换、不同精度之间的互转,在 RISC-V 中通过统一的 fcvt 指令族完成:

asm
# 浮点 → 整数
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 异常标志,并返回适当值(最大/最小/无穷大)。

asm
# 示例:华氏温度转摄氏温度
# 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:

asm
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, rs2

R-type,opcode=1010011;funct3=010 为 feq,funct3=001 为 flt,funct3=000 为 fle。结果通过整数寄存器传递——这让你能用普通的 beq/bne/beqz/bnez 对浮点比较结果做分支:

asm
# if (fa0 < fa1) goto loop
flt.d  t0, fa0, fa1
bnez   t0, loop

与整数的 slt 逻辑一致——不设标志位,结果写寄存器,再配合分支指令做条件跳转。RISC-V 从始至终没有条件码寄存器。

NaN 行为

IEEE 754 规定任何与 NaN 的比较都应返回 false(无序比较)。RISC-V 严格遵循:

asm
# 设 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 法)

asm
# 用 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