16、RISC-V 汇编语言中的浮点运算编程指南

RISC-V 汇编语言中的浮点运算编程指南

1. GNU 调用约定

在 RISC-V 汇编语言编程里,GNU 调用约定为浮点寄存器赋予了 ABI 名称、用途以及保存责任,这和整数寄存器的情况类似。使用 ABI 寄存器名称有助于遵循正确的 GNU 寄存器约定。下面的表格列出了硬件浮点寄存器名称、ABI 名称以及保存责任:
| 寄存器 | ABI 名称 | 描述 | 保存者 |
| — | — | — | — |
| f0 - f7 | ft0 - ft7 | 浮点临时寄存器 | 调用者 |
| f8 - f9 | fs0 - fs1 | 浮点保存寄存器 | 被调用者 |
| f10 - f11 | fa0 - fa1 | 浮点参数/返回值 | 调用者 |
| f12 - f17 | fa2 - fa7 | 浮点参数 | 调用者 |
| f18 - f27 | fs2 - fs11 | 浮点保存寄存器 | 被调用者 |
| f28 - f31 | ft8 - ft11 | 浮点临时寄存器 | 调用者 |

2. 浮点控制和状态寄存器(fcsr)

除了浮点寄存器文件之外,还有一个 32 位的状态寄存器,名为 fcsr。这个寄存器包含一个模式和累积异常标志。下面是 fcsr 寄存器的结构:

24  3  1  1  1  1  1
NV DZ OF UF NX
31       8 7  5 4  3 2  1 0
Reserved  Rounding Mode (frm)  Accrued Exception (fflags)

fcsr.frm 字段用于设置默认的舍入模式,当操作码指定为 “dyn” 时会使用该模式。累积异常字段 fcsr.fflags 会一直保持设置状态,直到被清除。这样一来,程序员既可以在每个操作码执行后测试这些标志,也可以在计算结束时进行测试。

可以使用以下伪操作来加载和修改 fcsr 的值:

frcsr    rd           # rd = fcsrr
fscsr    rd,rs1        # rd = original fcsr, fscr = rs1

“frcsr” 操作只是将 fcsr 的值读取到目标寄存器中。而 “fscsr” 操作会先将原始的 fcsr 值加载到目标寄存器,然后用整数寄存器 rs1 中的值替换 fcsr 的值。

保留字段是为其他标准扩展预留的。如果这些扩展不存在,实现应该忽略对这些位的写入操作,并且在读取时返回零值。标准软件应该保留这些位的内容。

2.1 舍入模式 fcsr.frm

浮点舍入模式可以由 fcsr.frm 字段控制,也可以由指令操作码本身控制。下面的表格列出了各种舍入模式及其编码:
| 舍入模式 | 助记符 | 含义 |
| — | — | — |
| 000 | rne | 四舍五入到最接近的值,若为中间值则舍入到偶数 |
| 001 | rtz | 向零舍入 |
| 010 | rdn | 向下舍入(向负无穷方向) |
| 011 | rup | 向上舍入(向正无穷方向) |
| 100 | rmm | 四舍五入到最接近的值,若为中间值则舍入到绝对值最大的值 |
| 101 | 保留供未来使用 | |
| 110 | 保留供未来使用 | |
| 111 | dyn | 动态舍入模式:使用指令的 rm 字段选择舍入模式 |

GNU 汇编器能够识别上述表格中的助记符,可将其作为操作码的可选舍入模式参数。不过,如果要在 fsrmi 中以立即数形式加载舍入模式,就必须将立即数指定为数值常量,或者声明一个具有正确值的符号。例如,可以声明一个符号 .equ rmm,4 ,然后使用 fsrmi x0,rmm

以下是一些直接操作 fcsr.frm 字段的伪操作码:

frrm     rd 
# rd = fcsr.frm
fsrm     rd,rs1 
# rd = fcsr.frm, fcsr.frm=rs1
fsrmi    rd,imm 
# rd = fcsr.frm, fcsr.frm=imm

“frrm” 操作码只是将 fcsr.frm 的值复制到目标整数寄存器(加载值左侧的位会被设置为零)。”fsrm” 操作码同样会将原始的 fcsr.frm 值加载到目标整数寄存器,然后用 rs1 中的值设置 fcsr.frm。”fsrmi” 操作码与之类似,只是 fcsr.frm 是由立即数设置的。

例如,以下指令会将当前的 fcsr.frm 值加载到寄存器 a2,同时用 t3 的当前值设置 fcsr.frm:

fsrm     a2,t3 
# a2 = fcsr.frm, fcsr.frm=t3

2.2 累积异常标志 fcsr.fflags

累积异常标志的含义如下表所示:
| 标志助记符 | 标志含义 |
| — | — |
| NV | 无效操作 |
| DZ | 除以零 |
| OF | 溢出 |
| UF | 下溢 |
| NX | 不精确 |

为了方便操作,提供了以下伪操作码:

frflags  rd 
# rd = fcsr.fflags
fsflags  rd,rs1 
# rd = fcsr.fflags, fcsr.fflags = rs1
fsflagsi rd,imm 
# rd = fcsr.fflags, fcsr.fflags = imm

“frflags” 伪操作码会将标志加载到目标整数寄存器(标志左侧的位都会被设置为零)。”fsflags” 操作码同样会将标志加载到目标寄存器,同时用整数寄存器 rs1 中的值设置标志。而 “fsflagsi” 操作码的功能与之相同,只是 fcsr.fflags 是由立即数设置的。

3. NaN 生成和传播

浮点处理相对复杂且容易出错。许多数学运算会产生 NaN(非数字)和无穷值(正无穷和负无穷)。若想了解相关规则的更多信息,可以参考 RISC-V 标准文档和 IEEE 浮点格式。目前,只需了解这些特殊值的存在即可。

4. 操作码和数据格式

浮点操作码可以处理不同的浮点数据格式,这些格式如下表所示:
| fmt 字段 | 助记符 | 含义 | 位数 | 扩展 |
| — | — | — | — | — |
| 00 | S | 单精度 | 32 | F |
| 01 | D | 双精度 | 64 | D |
| 10 | H | 半精度 | 16 | V |
| 11 | Q | 四精度,需要 RV64IFD | 128 | Q |

许多浮点操作码的通用格式如下:

fopcode.{S|D|H|Q} rd,rs1,rs2[,rm]
fopcode.{S|D|H|Q} rd,rs1[,rm]

大括号表示格式选择(参考上面的表格),方括号表示可选的舍入模式(参考舍入模式表格)。例如:

fadd.s  fa0,ft1,ft2,rmm 
# add single-precision, round nearest
fsqrt.d ft1,fa0,rup 
# sqrt double-precision, round up

当舍入模式未指定或者指定为 “dyn” 时,使用的舍入模式由 fcsr.frm 决定。

5. 加载和存储

为了直接从内存中加载浮点值或者将其存储到内存中,可以使用以下操作码。其中,X 必须是表中的 W、D 或 Q 之一:

flX     rd,imm(rs1)   
# rd = load imm(rs1)
fsX     rs1,imm(rs2) 
# store imm(rs2) = rs1

例如,如果寄存器 a1 包含一个双精度值的指针,那么可以使用以下指令将其加载到寄存器 fa2 中:

fld     fa2,0(a1) 
# fa2 = load @ a1

6. 浮点计算

以下是一些基本的浮点操作码,其中格式 “F” 可以是 S、D、H 或 Q 之一。这些操作码可以在最后一个参数位置添加可选的舍入模式:

fadd.F    rd,rs1,rs2 
# rd = rs1 + rs2
fsub.F    rd,rs1,rs2 
# rd = rs1 – rs2
fmul.F    rd,rs1,rs2 
# rd = rs1 * rs2
fdiv.F    rd,rs1,rs2 
# rd = rs1 / rs2
fmin.F    rd,rs1,rs2 
# rd = min(rs1,rs2)
fmax.F    rd,rs1,rs2 
# rd = max(rs1,rs2)
fsqrt.F   rd,rs1 
# rd = square root of rs1

例如,以下指令使用 rmm 舍入模式将 fa0 除以 ft0,并将结果存储到 fa2 中:

fdiv.d    fa2,fa0,ft0,rmm

此外,RISC-V 还提供了“融合乘加”操作,这些操作需要第三个操作数 rs3,并且可以添加可选的舍入模式:

fmuladd.F    rd,rs1,rs2,rs3   # rd = rs1 * rs2 + rs3
fmulsub.F    rd,rs1,rs2,rs3   # rd = rs1 * rs2 – rs3
fnmulsub.F   rd,rs1,rs2,rs3   # rd = -rs1 * rs2 + rs3
fnmuladd.F   rd,rs1,rs2,rs3   # rd = -rs1 * rs2 – rs3

例如,以下指令使用向零舍入模式:

fmuladd.s    ft2,fa0,fa1,ft3,rtz    # ft2 = fa0 * fa1 + ft3

7. 转换操作

还提供了用于硬件转换浮点值和整数之间的操作码。整数格式必须是 W、WU、L 或 LU 之一,具体如下表所示:
| 助记符 | 含义 |
| — | — |
| W | 32 位有符号字 |
| WU | 32 位无符号字 |
| L | 64 位有符号字 |
| LU | 64 位无符号字 |

“fcvt” 操作码的通用格式如下,其中 F 可以是 H、S、D 或 Q 之一,”int” 来自上面的表格,并且可以添加可选的舍入模式:

fcvt.int.F   rd,rs1[,rm]     # Convert from integer -> float
fcvt.F.int   rd,rs1[,rm]     # Convert from float -> integer

例如,以下指令将无符号整数寄存器 a0 中的值转换为单精度浮点值并存储到 ft0 中:

fcvt.wu.s    ft0,a0          # ft0 = float(a0), single-precision from 32-bit a0

另一个例子是将单精度浮点值 ft4 转换为 32 位有符号字并存储到整数寄存器 a0 中:

fcvt.s.w     a0,ft4          # a0 = int32(ft4), from single-precision ft4

7.1 浮点零

与整数寄存器 x0 不同,浮点运算没有专门的零寄存器。要在浮点寄存器中创建零值,可以使用以下方法之一:

fcvt.s.w    rd,x0           # Set single-precision fp register rd to 0.0
fcvt.d.l    rd,x0           # Set double-precision fp register rd to 0.0

这两个创建浮点零值的例子不会引发异常。

7.2 转换失败

由于数据类型的限制,从浮点格式转换为整数格式容易出现问题。如果在舍入后,值无法用目标格式表示,那么会将其截断为最接近的值,并设置 NV 标志(表示无效操作)。例如,以下操作码可能会受到影响:

fcvt.lu.d   t2,fa3 
# Convert from float fa3 -> integer t2

下面的表格列出了转换失败时可能的结果:
| 描述 | fcvt.w.s | fcvt.wu.s | fcvt.l.s | fcvt..lu.s |
| — | — | — | — | — |
| 舍入后最小有效输入 | –231 | 0 | –263 | 0 |
| 舍入后最大有效输入 | 231–1 | 232–1 | 263–1 | 264–1 |
| 超出范围的负输入输出 | –231 | 0 | –263 | 0 |
| 负无穷输入输出 | –231 | 0 | –263 | 0 |
| 超出范围的正输入输出 | 232–1 | 232–1 | 263–1 | 264–1 |
| 正无穷或 NaN 输入输出 | 232–1 | 232–1 | 263–1 | 264–1 |

8. 浮点符号操作

为了方便程序员处理浮点符号,提供了以下符号注入操作码,其中 F 可以是 S、D 或 Q 之一:

fsgnj.F    rd,rs1,rs2 
# rd = |rs1| with sign(rs2) 
fsgnjn.F   rd,rs1,rs2 
# rd = |rs1| with opposite_sign(rs2)
fsgnjx.F   rd,rs1,rs2 
# rd = |rs1| with sign(rs1) xor sign(rs2)

例如,以下指令将 ft0 的绝对值以双精度格式加载到 fa0 中,并使用 ft1 的符号:

fsgnj.d    fa0,ft0,ft1

这些操作码不会引发异常标志。

还有一对伪操作码 “fneg” 和 “fabs” 利用了符号注入操作码,其中 F 可以是 S、D 或 Q 之一:

fneg.F     rx,ry 
# equivalent: fsgnjn.F rx,ry,ry
fabs.F     rx,ry 
# equivalent: fsgnjx.F rx,ry,ry

9. 浮点移动操作

如果数据值已经是 IEEE 754 - 2008 浮点格式,可以直接将其从整数寄存器复制到浮点寄存器,或者反之。要将这些值从浮点寄存器 (rs1) 移动到整数寄存器 (rd),可以使用以下操作码之一:

mv.x.w rd,rs1 
# rd = rs1 single-precision
fmv.x.d rd,rs1 
# rd = rs1 double-precision (RV64)

要将浮点表示数据从整数寄存器 (rs1) 移动到浮点寄存器 (rd),可以使用以下操作码之一:

fmv.w.x rd,rs1 
# rd = rs1 single-precision
fmv.d.x rd,rs1 
# rd = rs1 double-precision (RV64)

需要注意的是,”d” 操作码版本需要 RV64 或更高的扩展支持,这要求整数寄存器宽度为 64 位。

另外,RISC-V 规范指出:“FMV.W.X 和 FMV.X.W 指令以前被称为 FMV.S.X 和 FMV.X.S。使用 W 更符合它们作为不解释 32 位数据的指令的语义。在定义 NaN 装箱之后,这一点变得更加清晰。为了避免影响现有代码,工具将同时支持 W 和 S 版本。”

10. 浮点比较操作

可以通过将比较的布尔结果存储在目标整数寄存器中来比较浮点值。以下操作码中,F 必须是 S、D 或 Q 之一:

feq.F    rd,rs1,rs2 
# rd = rs1 == rs2
flt.F    rd,rs1,rs2 
# rd = rs1 < rs2
fle.F    rd,rs1,rs2 
# rd = rs1 <= rs2

在 “flt” 和 “fle” 操作码中,如果任何一个输入是 NaN,则会引发无效操作 (NV) 异常,因为无法进行有效的比较。对于 “feq” 操作码,只有信号 NaN (sNaN) 会引发无效操作 (NV) 异常。当任何一个操作数为 NaN 值时,这三个操作码都会返回布尔假(零)。

需要注意的是,信号 NaN (sNaN) 的作用是在调试时引发异常(可能是因为未初始化的值)。算术运算不会产生 sNaN,但可能会产生安静 NaN 值。

11. 分类操作

浮点分类操作可以在一次操作中快速方便地对浮点值进行分类。将下面的 “F” 替换为精度指定符 S、D 或 Q:

fclass.F   rd,rs1 
# rd = classify(rs1)

目标寄存器 rd 是一个整数寄存器,它会接收如下表所示的 8 位结果:
| rd 位 | 含义 |
| — | — |
| 0 | rs1 是 -∞ |
| 1 | rs1 是负的正常数 |
| 2 | rs1 是负的非正规数 |
| 3 | rs1 是 -0 |
| 4 | rs1 是 +0 |
| 5 | rs1 是正的非正规数 |
| 6 | rs1 是正的正常数 |
| 7 | rs1 是 +∞ |
| 8 | rs1 是信号 NaN (sNaN) |
| 9 | rs1 是安静 NaN |

12. 华氏温度转摄氏温度示例

接下来,我们将应用所学知识,把原来基于整数的程序转换为使用浮点运算来计算华氏温度到摄氏温度的转换。转换公式如下:
[C = \frac{F - 32}{1.8}]
这里我们将使用 Fedora Linux 项目和 QEMU,并且硬件支持 F 和 D 扩展的浮点运算。以下是汇编部分的代码:

1  #       The floating-point version of conftemp (fconvtemp)
2          .global fconvtemp
3          .text
4  
5          .equ    rtz,0x1                 # Round to zero
6          .equ    rmm,0x4                 # Round to Nearest
7          .equ    dyn,0x7                 # Dynamic rounding mode
8  #
9  #       extern double fconvtemp(double fahrenheit,unsigned *pflags)
10  #
11  # ARGUMENTS:
12  #       fa0     temperature in Fahrenheit
13  #       a0      pointer to int to return flags
14  #
15  # RETURNS:
16  #       fa0     temperature in Celsius
17  #       flags through ptr in a0
18  
19  fconvtemp:
20          frcsr   t2                      # t2 = original fcsr
21          fsrmi   x0,rmm                  # Set rnd mode to RMM
22          fsflagsi x0,0                   # Clear exceptions
23          la      t4,f18
24          fld     ft0,0(t4)               # ft0 = 1.8
25          addi    t0,x0,32                # t0 = 32
26          fcvt.d.lu ft1,t0,rtz            # ft1 = 32.0
27  
28  conv:   fsub.d  fa0,fa0,ft1,rtz         # fa0 -= 32.0
29          fdiv.d  fa0,fa0,ft0,rmm         # fa0 /= 1.8
30  
31          frflags t0                      # t0 = fcsr.flags
32          sw      t0,0(a0)                # Store fcsr.flags
33  
34          fscsr   x0,t2                   # Restore fscr
35          ret     
36  
37          .section .rodata
38  f18:    .double 1.8

下面是对这段代码的详细解释:
1. 函数参数
- 函数接收两个参数,一个是包含华氏温度的双精度值,会通过硬件寄存器 fa0 传入。
- 另一个是指向无符号整数的指针,用于在计算完成后返回 fcsr.flags。
2. 保存原始 fcsr 值 :第 20 行将当前的 fcsr 值加载到整数寄存器 t2 中,以便在函数返回时恢复 fcsr。
3. 设置舍入模式 :第 21 行设置默认舍入模式为“四舍五入到最接近的值”,以防指令中未指定舍入模式。
4. 清除异常标志 :第 22 行将异常寄存器设置为零,清除所有异常标志。
5. 加载常量地址 :第 23 行使用“加载地址”伪操作在 t4 中建立一个地址,用于访问第 38 行的双精度常量。
6. 加载常量值 :第 24 行将双精度值 1.8 加载到浮点寄存器 ft0 中,供后续使用。
7. 设置整数常量 :第 25 行在临时寄存器 t0 中设置整数常量 32。
8. 整数转换为浮点 :第 26 行将整数 32 转换为浮点值,使用“向零舍入”模式。
9. 进行温度转换计算
- 第 28 行从传入的华氏温度值中减去 32.0,使用“向零舍入”模式,并将结果存储回 fa0。
- 第 29 行将 fa0 除以 1.8,使用“四舍五入到最接近的值”模式,得到摄氏温度值并存储在 fa0 中,这将作为函数的返回值。
10. 获取并返回异常标志
- 第 31 行将 fcsr.flags 加载到临时寄存器 t0 中。
- 第 32 行通过传入的指针将 t0 中的标志返回给调用者。
11. 恢复 fcsr 值 :第 34 行将 fcsr 恢复到原始状态,以满足调用者(如 C/C++ 代码)的需求。
12. 函数返回 :第 35 行函数返回,浮点寄存器 fa0 中存储着转换后的摄氏温度值。

对于这样一个简单的计算,这个过程可能看起来有些繁琐。但如果是复杂的科学计算,这种方法可以在每一步都精确控制舍入,而在 C/C++ 中,通常会选择一个默认的舍入模式并贯穿整个计算过程。

下面是主程序的代码:

1  #include <stdio.h>
2  
3  extern double fconvtemp(double f,unsigned *pflags);
4  
5  int
6  main(int argc,char **argv) {
7          static double const tests[] = {
8                  32.0, 0.0, -40.0, 18.5
9          };
10  
11          for ( int ux=0; ux < 4; ++ux ) {
12                  unsigned flags;

这个主程序定义了一个包含四个测试温度值的数组,并通过循环依次调用 fconvtemp 函数进行温度转换。在每次循环中,会声明一个无符号整数 flags 用于接收转换过程中的异常标志。后续代码可以继续完善,例如打印转换后的温度和异常标志,以验证转换的正确性。

通过上述内容,我们详细介绍了 RISC-V 汇编语言中浮点运算的各个方面,包括寄存器约定、舍入模式、异常处理、数据格式、操作码以及实际应用示例。掌握这些知识可以帮助开发者在 RISC-V 平台上进行高效、精确的浮点计算编程。

13. 代码调用流程分析

为了更清晰地理解华氏温度转摄氏温度的代码执行过程,我们可以通过 mermaid 流程图来展示其调用流程:

graph TD
    A[开始] --> B[保存原始 fcsr 值]
    B --> C[设置舍入模式]
    C --> D[清除异常标志]
    D --> E[加载常量地址]
    E --> F[加载常量值]
    F --> G[设置整数常量]
    G --> H[整数转换为浮点]
    H --> I[进行温度转换计算]
    I --> J[获取并返回异常标志]
    J --> K[恢复 fcsr 值]
    K --> L[函数返回]
    L --> M[结束]

从这个流程图可以直观地看到,函数从开始执行后,按照一系列步骤完成温度转换,最后恢复状态并返回结果。

14. 浮点运算的注意事项

在进行 RISC-V 浮点运算编程时,有以下几点需要特别注意:
- 舍入模式的选择 :不同的舍入模式会对计算结果产生影响。在实际应用中,需要根据具体的需求选择合适的舍入模式。例如,在金融计算中,可能需要更精确的舍入方式;而在一些对精度要求不高的场景下,可以选择较为简单的舍入模式。
- 异常标志的处理 :累积异常标志(fcsr.fflags)记录了浮点运算过程中的异常情况。在重要的计算中,应该及时检查这些标志,以便发现和处理可能出现的问题,如无效操作、除以零、溢出等。
- 数据格式的匹配 :在进行数据加载、存储和转换操作时,要确保数据格式的匹配。例如,在使用 flX fsX 操作码时,要根据实际情况选择正确的 X 值(W、D 或 Q);在进行整数和浮点之间的转换时,也要注意目标格式是否能够正确表示转换后的值。

15. 总结

通过对 RISC-V 汇编语言中浮点运算的学习,我们了解了许多重要的知识点,包括:
1. 寄存器约定 :GNU 调用约定为浮点寄存器分配了 ABI 名称、用途和保存责任,有助于编写规范的代码。
2. fcsr 寄存器 :包含舍入模式和累积异常标志,可通过伪操作进行加载和修改。
3. 舍入模式 :有多种舍入模式可供选择,可通过 fcsr.frm 字段或指令操作码控制。
4. 操作码和数据格式 :浮点操作码可以处理不同的浮点数据格式,如单精度、双精度等。
5. 加载和存储操作 :使用特定的操作码可以直接从内存中加载和存储浮点值。
6. 浮点计算和转换操作 :提供了基本的浮点运算操作码和整数与浮点之间的转换操作码。
7. 符号操作和比较操作 :方便处理浮点符号和进行浮点值的比较。
8. 分类操作 :可以快速对浮点值进行分类。
9. 实际应用示例 :通过华氏温度转摄氏温度的示例,展示了如何在实际编程中应用这些知识。

通过掌握这些知识,开发者可以在 RISC-V 平台上进行高效、精确的浮点计算编程。同时,在实际应用中要注意舍入模式的选择、异常标志的处理和数据格式的匹配等问题,以确保程序的正确性和稳定性。

16. 扩展思考

在掌握了基本的浮点运算编程之后,我们可以进一步思考一些扩展应用和优化方向:
- 性能优化 :对于复杂的浮点计算,可以考虑使用并行计算或优化算法来提高性能。例如,利用 RISC-V 的多核特性进行并行计算,或者采用更高效的数值算法来减少计算量。
- 错误处理和容错机制 :在实际应用中,可能会遇到各种异常情况。可以设计更完善的错误处理和容错机制,确保程序在出现异常时能够稳定运行,并提供有用的错误信息。
- 跨平台兼容性 :如果需要在不同的 RISC-V 平台上运行程序,要考虑平台之间的差异,如硬件浮点支持、指令集扩展等,确保程序具有良好的跨平台兼容性。

通过不断地学习和实践,我们可以更好地利用 RISC-V 汇编语言进行浮点运算编程,开发出更加高效、稳定和功能强大的应用程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值