第三章:计算机中的算术

一、整数表示(Integer Representation)

在计算机系统中,整数主要有两种表示方式:无符号整数有符号整数(补码)

1.1 无符号整数 (Unsigned Binary Integers)

无符号整数直接将二进制位权相加,仅用于表示非负数。

(1) 表示公式
对于一个 \(n\) 位二进制数:

\[x = x_{n-1}2^{n-1} + x_{n-2}2^{n-2} + \dots + x_12^1 + x_02^0\]

(2) 数值范围
\(0\)\(+(2^n - 1)\)

(3) 32位系统示例
范围\(0\)\(+4,294,967,295\)
二进制转十进制:例如 0000...1011₂ 对应 \(1\times2^3 + 0\times2^2 + 1\times2^1 + 1\times2^0 = 8 + 0 + 2 + 1 = 11_{10}\)

1.2 补码有符号整数 (2s-Complement Signed Integers)

补码是计算机中表示有符号(正、负、零)整数的标准方法,其核心特点是最高位(MSB)具有负权重

(1) 表示公式
对于一个 \(n\) 位数:

\[x = \color{red}{-x_{n-1}2^{n-1}} + \color{black}{x_{n-2}2^{n-2}} + \dots + x_{1}2^1 + x_{0}2^0\]

(2) 符号位 (Sign Bit)
在32位整数中,第31位是符号位:
0:表示非负数(Non-negative numbers)。
1:表示负数(Negative numbers)。

(3) 数值范围
\(-2^{n-1}\)\(+(2^{n-1} - 1)\)

(4) 32位系统示例
范围\(-2,147,483,648\)\(+2,147,483,647\)
计算示例\(1111...1100_2\) 对应 \(-1\times2^{31} + 1\times2^{30} + \dots + 1\times2^2 + 0\times2^1 + 0\times2^0 = -4_{10}\)

补码的关键特性:

  1. 非负数一致性:非负数的补码表示与其无符号表示完全相同。
  2. 特定的重要数值\(N\)位):
    • 0\(0000 0000 ... 0000\)
    • -1\(1111 1111 ... 1111\)(所有位全为1)
    • 最大正数 (Most-positive)\(0111 1111 ... 1111\) (\(2^{N-1}-1\))
    • 最小负数 (Most-negative)\(1000 0000 ... 0000\) (\(-2^{N-1}\),其绝对值比最大正数大1)
  3. 符号位扩展:同一个数值在不同位宽表示下,低位数值位完全相同,符号位扩展。例如,\(-20\)
    • 8 位表示\(\textcolor{red}{1110\ 1100}_2\)
    • 16 位表示\(1111\ 1111\ \textcolor{red}{1110\ 1100}_2\)
    • 32 位表示\(1111\ 1111\ 1111\ 1111\ 1111\ 1111\ \textcolor{red}{1110\ 1100}_2\)

1.3 符号取反操作 (Signed Negation)

补码系统中,将一个数变号(正数变负数或反之)的捷径是:求反加一

(1) 操作步骤

  1. 取反 (Complement):将所有位翻转(1变0,0变1)。
  2. 加1:在最低位加1。

(2) 数学原理

\[\bar{x} + 1 = -x\]

证明
对于一个 \(n\) 位数:
\(x = \color{red}{-x_{n-1}2^{n-1}} \color{black}{+ x_{n-2}2^{n-2} + \dots + x_{1}2^1 + x_{0}2^0}\)
我们将其各位取反
\(\bar{x} = \color{red}-{\bar{x}_{n-1}2^{n-1}} \color{black}{+ \bar{x}_{n-2}2^{n-2} + \dots + \bar{x}_{1}2^1 + \bar{x}_{0}2^0}\)
注意到:
\(x_{k} + \bar{x}_{k} = 1\)
因此我们有:

\[\begin{aligned} x + \bar{x} &= \color{red}{(-x_{n-1} - \bar{x}_{n-1})2^{n-1}} + \color{black}{(x_{n-2} + \bar{x}_{n-2})2^{n-2} + \dots + (x_{1} + \bar{x}_{1})2^1 + (x_{0} + \bar{x}_{0})2^0} \\ &= \color{red}{-2^{n-1}} + \color{black}{2^{n-2} + \dots + 2^1 + 2^0} \\ &= \color{red}{-2^{n-1}} + \color{black}{(2^{n-1} - 1)} \\ &=-1 \\ \end{aligned}\]

证毕。
(3) 示例:将 +2 变为 -2

  • \(+2\) = 0000...0010₂
  • 第一步取反:1111...1101₂
  • 第二步加1:1111...1101₂ + 1 = 1111...1110₂ (即 -2)。

进而引申:

Theorem 1.

计算机中,减去一个正数,等于加上它的相反数的补码

Theorem 2.

计算机中,加法和减法都可以通过补码的加法实现

二、整数加减法与溢出处理

2.1 整数加减法 (Integer Addition & Subtraction)

  • 加法原则:从最低位开始逐位相加,并向高位进位(Carries)。
  • 减法原则:通过加法器实现。减去一个数等于加上该数的补码(即取反加1)。
    • 例如:\(7 - 6 = 7 + (-6)\)

2. 溢出处理 (Dealing with Overflow)

当运算结果超出了当前位数所能表示的范围时,发生溢出。

  • 加法溢出判定
    • 一正一负相加永远不会溢出
    • 两个正数相加:结果符号位为1(变为负数),则溢出。
    • 两个负数相加:结果符号位为0(变为正数),则溢出。
  • 减法溢出判定
    • 两个同号数相减永远不会溢出
    • 负数减正数:结果符号位为1,则溢出。
    • 正数减负数:结果符号位为0,则溢出。
  • 两种溢出处理方式
    • Satruating(饱和):当发生溢出时,结果被强制设置为可表示的最大值或最小值。常见于Graphics and media processing
    • Clipping(截断):当发生溢出时,结果被截断为可表示的范围内的值。即取模回绕(Wrap-around)。例如在8bit无符号加法中,\(255 + 1\) 会回绕到 \(0\)

三、乘法运算及其电路实现

3.1 基础乘法算法 (Long-multiplication Approach)

类似于手工长乘法,通过逐位检查乘数(Multiplier)并对被乘数(Multiplicand)进行移位和累加

  • 硬件特点乘积的长度是操作数长度之和(如两个32位数相乘得到64位积)。
  • 初步硬件实现:包含64位ALU、64位被乘数寄存器(左移)、32位乘数寄存器(右移)及64位乘积寄存器。缺点:时钟周期多,硬件利用率低。

alt text

3.2 优化后的乘法器电路 (Optimized Multiplier)

为了减小面积和提升速度,芯片设计中常采用优化电路:

  • 并行化设计:将加法和移位步骤并行执行
  • 寄存器优化:乘积寄存器的右半部分最初存放乘数,随着运算进行,乘数被移出,乘积的低位被移入,从而节省空间。

alt text

3.3 高性能“真实”乘法器 (Real Multiplier Structure)

现代芯片(如GPU/高性能CPU)中使用更复杂的电路结构:

  • Booth Recoding:通过对乘数进行编码,减少产生的部分积(Partial Products)数量。
  • Wallace Tree/Dadda Tree:采用树状结构的加法器阵列,实现部分积的并行压缩相加,大幅降低延迟。
  • 超前进位加法器 (Carry Look-ahead Adder, CLA):用于最后的求和阶段(CPA),以极快速度处理进位传递。

alt text

3.4 RISC-V 乘法指令实现

由于 RISC-V 的标准寄存器(在 32 位架构下)只能存储 32 位数据,因此指令集通过不同的指令来分别获取这 64 位结果的低 32 位高 32 位

(1) 获取乘积的低位 (Least-significant 32 bits)

  • 指令mul rd, rs1, rs2
  • 功能:将 rs1rs2 相乘,并将结果的低 32 位写入目标寄存器 rd
  • 特点:无论操作数是有符号数还是无符号数,乘积的低 32 位在二进制补码表示下都是相同的,因此不需要区分符号类型。

(2) 获取乘积的高位 (Most-significant 32 bits)
对于乘积的高 32 位,符号位的影响至关重要,因此 RISC-V 提供了三条不同的指令来处理符号扩展:

  • mulh rd, rs1, rs2:用于 有符号数 × 有符号数。它会将结果的高 32 位存入 rd
  • mulhu rd, rs1, rs2:用于 无符号数 × 无符号数。它将结果的高 32 位作为无符号数处理并存入 rd
  • mulhsu rd, rs1, rs2:用于 有符号数 × 无符号数。这种混合模式在处理多倍精度算术或特定算法时非常有用,它同样取结果的高 32 位。

四、整数除法(Division)运算及其电路实现

4.1 基础除法算法与恢复余数法 (Restoring Division)

除法的逻辑过程类似于手工长除法,但硬件实现更为具体:

  • 基本原则对于 \(n\) 位操作数,会产生一个 \(n\) 位的商(Quotient)和一个 \(n\) 位的余数(Remainder)
  • 算法步骤
    • 检查除数(Divisor)是否为0。
    • 比较除数与被除数(Dividend)的当前位,如果除数 \(\le\) 当前位,则商置1并进行减法。
    • 否则,商置0,并将被除数的下一位降下继续比较。
  • 恢复余数法 (Restoring Division):在硬件执行减法后,如果余数变为负数(< 0),则必须将除数加回到余数中以“恢复”原来的值。

4.2 除法器硬件电路实现 (Division Hardware)

课件对比了两种硬件结构,重点在于资源优化:

  • 非优化结构:使用64位ALU、64位余数寄存器和64位除数寄存器(右移),商寄存器为32位(左移)。

alt text

  • 优化后的除法器 (Optimized Divider)
    • 硬件高度复用:优化后的除法器结构与乘法器非常相似,通常可以使用同一套硬件来实现乘法和除法。
    • 寄存器配置:32位ALU;64位余数寄存器初始化时右半部分放被除数,运算结束后左半部分存放余数,右半部分存放商。

alt text

4.3 除法加速技术 (Faster Division)

与乘法不同,除法很难使用类似华莱士树的并行硬件来加速。常用的加速手段包括:
(1) 不恢复余数法 (Non-restoring Division)
我们就假设减掉除数后余数小于0的情况

Method reminder of cycle \(N\) reminder of cycle \(N + 1\)
Restoring \(r - d(<0)\) restore to \(r\) \(r \times 2 - d\)
Non-restoring \(r - d(<0)\) \(2(r-d) + d\)

(上面的乘2,原因是在做下一轮减法时,需要将余数左移一位,这样被除数的下一位才能落在“个位”,可以联想一下手工除法以作理解)

Non Restoring 的意思就是:
即使减法结果为负,也直接存储该差值,不进行“加回”操作。
通过在下一周期进行特定的补偿调整节省了恢复余数所需的时钟周期

(2) SRT 除法 (SRT Division)
每一步可以产生多个商位(例如 Radix-4 每步产生4位)。

实现原理:利用查找表(Lookup Table),根据余数的高几位和除数的高几位来“预测”需要减去的数值,从而减少总迭代步数。
alt text

  1. SRT 算法中:查找表会存储 \(m\) 位的部分商,这些部分商是由部分余数的 \(m\) 位和除数的 \(m\)共同决定的。表格的存储空间大小为 \(2^{2m}\times m\) 位,这个大小会受到内存容量的限制。
  2. 采用基数为 \(2b\) 的除法运算,可以将所需的计算周期数降至原来的 \(1/b\),但每个周期的实现复杂度也相应增加了。

4. 有符号除法 (Signed Division)

有符号除法的核心原则是“余数的符号必须与被除数一致”:

  1. 取被除数和除数的绝对值进行除法运算。
  2. 调整符号规则
    • 如果操作数异号,则商为负。
    • 余数的符号必须与被除数一致
    • 这意味着商总是向零舍入(Rounded toward zero)

5. RISC-V 除法指令实现

RISC-V 将商和余数的获取拆分为不同的指令,且区分有符号与无符号:

  • 获取商 (Quotient)
    • div rd, rs1, rs2:有符号除法。
    • divu rd, rs1, rs2:无符号除法。
  • 获取余数 (Remainder)
    • rem rd, rs1, rs2:有符号取模。
    • remu rd, rs1, rs2:无符号取模。

五、浮点数表示与编码 (Floating-Point Representation)

浮点数用于表示非整数,包括极大或极小的数值,其形式类似于科学计数法。

5.1 二进制科学计数法 (Binary Scientific Notation)

在二进制中,浮点数表示为:\(\pm 1.xxxxxxx_2 \times 2^{yyyy}\)

  • 规格化 (Normalized):基数点左侧只有一位非零数字(在二进制中恒为1)。
  • 非规格化 (Not normalized):如 \(+0.002 \times 10^{-4}\)

5.2 IEEE 754 浮点数标准

这是目前计算机系统通用且几乎唯一采用的浮点数格式。它定义了两种主要表示形式:

  • 单精度 (Single precision, float):32位。
  • 双精度 (Double precision, double):64位。

编码结构图示:

精度 符号位 (S) 指数位 (Exponent) 尾数位 (Fraction)
单精度 1 bit 8 bits 23 bits
双精度 1 bit 11 bits 52 bits

5.3 核心计算公式

浮点数 \(x\) 的数值计算如下:

\[\mathbf{x = (-1)^S \times (1 + Fraction) \times 2^{(Exponent - Bias)}}\]

关键编码原则:

  • 符号位 (S):0 表示非负,1 表示负数。
  • 隐含位 (Hidden Bit):为了节省空间,规格化后的尾数总是以“1.”开头,因此硬件编码时只存储点后的部分 (Fraction)计算时自动恢复“1.”
  • 偏置指数 (Biased Exponent)
    • 指数部分采用偏置计数法,确保指数始终为无符号数,便于硬件比较大小。
    • Bias 值:单精度为 127,双精度为 1023
    • 实际指数 = 寄存器值 (Exponent) - Bias。

Question

为什么需要bias?

在浮点数表示中,指数 \(E\) 本身既可能是正数,也可能是负数(例如 \(2^{-126}\)\(2^{127}\))。如果直接用补码表示,硬件进行比较和排序时会变得复杂
通过引入偏置 \(B\),我们将实际指数 \(e\) 映射为存储的指数 \(E\)

\[E = e + B\]

这样我们能够确保 \(E\) 始终是一个非负整数。

为什么是127和1023?

以单精度为例,规格化数(normalized numbers) 可用的 \(E\) 的范围是 \([1, 254]\)。为了让正负指数的范围尽可能对称,我们需要寻找一个中间值 \(B\),使得:当 \(E = 1\) 时,对应的实际指数 \(e_{min}\) 为负数。当 \(E = 254\) 时,对应的实际指数 \(e_{max}\) 为正数。且 \(e_{max} \approx |e_{min}|\)
代入公式即可解得:\(B = 127\)

5.4 数值范围与精度

(1) 单精度范围

  • 最小值:\(\color{blue}{1.00\cdots 0 \times 2^{1-127}} \color{black}{= \pm 1.0 \times 2^{-126} \approx \pm 1.2 \times 10^{-38}}\)
  • 最大值:\(\color{blue}{1.11\cdots 1 \times 2^{254-127}} \color{black}{\approx \pm 2.0 \times 2^{+127} \approx \pm 3.4 \times 10^{+38}}\)

(2) 浮点数稠密度

浮点数不是均匀分布的。它是一个非均匀的线性分段结构。
单精度浮点数的形式为:\((-1)^S \times (1.M) \times 2^{E-127}\)

  • 指数 (E) 决定了“量级”(区间)。
  • 尾数 (M) 决定了该区间内的“划分密度”。

在一个给定的指数 \(E\) 下,尾数有 23 位,意味着该区间内有 \(2^{23}\) 个等间距的点。随着 \(E\) 的增大,区间的跨度变大,但点数始终为 \(2^{23}\),因此相邻点的间距(即最小精度单位,称为 ULP,Unit in the Last Place)会随指数增大而按指数增长。

(3) 相对精度

  • 单精度:约为 \(2^{-23} \approx 1.19 \times 10^{-7}\),即大约 7 位十进制有效数字。
  • 双精度:约为 \(2^{-52} \approx 2.22 \times 10^{-16}\),即大约 16 位十进制有效数字。

(4) 浮点数相对间距
对于指数 \(E\)(规格化数),相邻两个浮点数之间的差值为:

\[\text{ULP} = 2^{E-127} \times 2^{-23} = 2^{E-127-23} = 2^{E-150}\]
  • 例子 1:指数相同时
    假设 \(E= e + 127\)(即实际指数为 \(e\))。
    此时 \(2^{e} \times 2^{-23} = 2^{e-23}\)
    在这个区间内,每个数之间都隔着 \(2^{e-23}\)

  • 例子 2:指数相差 1 时(跨过 \(2^n\) 分界点)
    这是浮点数最容易让人误解的地方。当指数 \(E\) 增加 1 时,该区间的数值跨度翻倍,因此 ULP 也翻倍

    • 在区间 \([2^0, 2^1)\),即 \(E=127\) 时,ULP 为 \(2^{-23}\)
    • 在区间 \([2^1, 2^2)\),即 \(E=128\) 时,ULP 为 \(2^{1-23} = 2^{-22}\)

    结论:数值越大,绝对误差越大,但相对精度(精度与数值的比值)保持不变。

5.5 非规格化浮点数

5.5.1. 数学定义:为什么偏偏是 \(2^{-126}\)

前面我们提到,当指数位 \(E = 0\) 且尾数位 \(M \neq 0\) 时,该数为非规格化数。
它的计算公式是:

\[V = (-1)^S \times (0.M) \times 2^{-126}\]

你可能会问:既然 \(E=0\),偏置是 \(127\),按照公式 \(e = E - B = 0 - 127 = -127\),为什么非规格化数的指数被硬性规定为了 \(-126\) 呢?

这是一个极度聪明的工程妥协,目的是为了与“最小的规格化数”实现无缝的平滑衔接

我们来看看这中间的过渡:

  • 最小的正规格化数\(E=1, M=00...0\)):
    数值 \(= 1.0 \times 2^{1-127} = 1.0 \times 2^{-126}\)

  • 最大的非规格化数\(E=0, M=11...1\)):
    数值 \(= 0.111...1_2 \times 2^{-126}\) (注意这里使用的是 \(-126\)

如果我们将非规格化数的指数设定为 \(-126\),那么从最大的非规格化数加 1 个 ULP(也就是加 \(0.000...1_2 \times 2^{-126}\)),刚好就等于 \(1.0 \times 2^{-126}\),完美过渡到了最小规格化数!
如果使用 \(-127\),这两者之间就会出现断层。因此,IEEE 754 规定非规格化数的实际指数“冻结”在最小规格化数的指数上(即 \(-126\)),仅仅将隐含位从 1 变成了 0。

5.5.2 核心使命:拯救“突然下溢”与捍卫数学公理

在没有非规格化数的早期计算机中,浮点数运算存在一个致命缺陷。

灾难场景:Flush-to-Zero(粗暴归零)
如果没有非规格化数,单精度能表示的最小正数就是 \(N_{min} = 1.0 \times 2^{-126}\)
如果进行减法 \(x - y\),比如:
\(x = 1.0000001_2 \times 2^{-126}\)
\(y = 1.0000000_2 \times 2^{-126}\)
很明显 \(x \neq y\)。但是它们相减的结果是 \(0.0000001_2 \times 2^{-126}\)
由于没有非规格化数(无法表示隐含位为 0 的数),计算机只能认定这个结果超出了表示范围(下溢),直接把它暴力截断为 0

这违背了代数中最基本的一条公理:
\(x - y = 0 \iff x = y\)

在数值分析中,如果 \(x \neq y\)\(x - y = 0\),会导致诸如 if (x != y) z = 1 / (x - y) 这样的代码发生除零崩溃(Divide by Zero)。

救星:逐渐下溢(Gradual Underflow)
非规格化数填补了 \(0\)\(N_{min}\) 之间的巨大鸿沟。
\(2^{23} - 1\) 个非规格化数,像极其细密的沙子一样填进了最靠近 0 的区间。
由于非规格化数的存在,只要 \(x\)\(y\) 是浮点数,\(x - y\) 的结果即使再小,也必定能用非规格化数精确表示出来。这就彻底保卫了 \(x - y = 0 \iff x = y\) 这个定理。

关于非规格化浮点数,更详细的介绍可以参考非规格化浮点数介绍文档

5.5.3 \(\pm 0\)

非规格化浮点数中,当尾数全为0时,表示正零\(+0\))或负零\(-0\)),取决于符号位。

5.6 Infinity and NaN

当指数全为1(\(E=255\))时,IEEE 754 预留了两种特殊数值:

  • 无穷大 (\(\pm \infty\)):当尾数全为0时,表示正无穷或负无穷(取决于符号位)。这通常用于表示溢出结果或除以零的情况。
  • NaN (Not a Number):当尾数非零时,表示非法数值(如 \(0.0/0.0\))。NaN 具有特殊的传播规则:任何与 NaN 进行的算术运算结果仍然是 NaN。

5.6 特殊数值汇总 (Reserved Values)

指数 (Exponent) 尾数 (Fraction) 表达对象 (Object)
0 0 0.0 (有 \(\pm 0.0\) 之分)
0 非零 非规格化数 (Denormal):用于表示极小值,允许精度阶梯式下降,此时\(e = -126(-1022)\)
1 至 254 (2046) 任意值 规格化浮点数
255 (2047) 0 无穷大 (\(\pm \infty\)):用于处理溢出,无需检查即可继续运算
255 (2047) 非零 NaN (Not a Number):表示非法结果(如 0.0/0.0)

六、浮点数加减法运算 (Floating-Point Addition & Subtraction)

浮点数的加减法比整数加减法要复杂得多,因为它们的小数点(量级)是不对齐的。其核心思想类似于科学计数法的加法(如:\(9.999 \times 10^1 + 1.610 \times 10^{-1}\))。

浮点数加减法(硬件执行)通常包含以下 四个标准步骤

6.1 算法步骤 (Algorithm Steps)

(1) 比较指数并对齐尾数 (Compare and Align)

  • 硬件操作:将两个操作数的指数相减,找出较小的一个
  • 对齐原则小阶向大阶看齐。将指数较小的那个数的 尾数(连同隐藏位 1) 向右移位(Right Shift),每右移一位,其指数加 1,直到两个指数相等。
  • 注意:右移过程中移出的低位数据可能会丢失,但硬件通常会保留几个保护位(后续详述)以保证精度。

(2) 尾数相加/相减 (Add/Subtract Significands)

  • 在指数对齐后,直接将两个带隐藏位的尾数(24位或53位)放入加法器/减法器中进行运算。

(3) 规格化结果 (Normalize Result)

  • 相加/相减后的结果可能不再符合 \(1.M\) 的规格化形式,需要进行调整:
    • 如果结果 \(>2\)(例如 \(1.1_2 + 1.1_2 = 11.0_2\)):将尾数向右移 1 位,同时指数加 1
    • 如果结果 \(<1\)(例如相减产生 \(0.0011_2\)):将尾数向左移位,直到最高位变成 1,左移了几位,指数就减几
  • 在此阶段,硬件还要检查溢出 (Overflow)(指数过大)或下溢 (Underflow)(指数过小,变为非规格化数或0)。

(4) 舍入 (Rounding)

  • 由于硬件位宽限制,规格化后的结果必须被截断/舍入到 23 位(单精度)或 52 位(双精度)。
  • 舍入后可能会再次破坏规格化(例如进位导致变成 \(10.0_2\)),如果发生这种情况,需要再次右移并调整指数

6.2 浮点加法器硬件电路特点

浮点加法器的电路比整数 ALU 复杂且庞大:

  1. 包含用于指数比较的减法器
  2. 包含用于尾数对齐和规格化的两个移位器 (Shifters)
  3. 包含一个宽带位的尾数加法器
  4. 通常由于逻辑级数过深,浮点加法在高性能流水线 CPU 中会分为多个时钟周期(Pipeline Stages)完成。

七、浮点数乘除法运算 (Floating-Point Multiplication & Division)

相比于加法,浮点数乘法不需要对齐尾数,但在指数处理上有一个极易踩坑的重点:Bias的处理

7.1 浮点乘法算法步骤

(1) 计算新指数并处理 Bias (Add Exponents)
对于两个浮点数 \(x_1\)\(x_2\),实际指数相乘等于相加:\(e_{result} = e_1 + e_2\)
但是在 IEEE 754 中,寄存器里存的是带偏置的指数 \(E\)\(E = e + Bias\))。
如果直接把两个寄存器的 \(E\) 相加:

\[E_1 + E_2 = (e_1 + \text{Bias}) + (e_2 + \text{Bias}) = e_1 + e_2 + 2 \times \text{Bias}\]

结果多出了一个 Bias!
因此,浮点乘法计算指数的公式为:

\[\mathbf{E_{result} = E_1 + E_2 - \text{Bias}}\]

(2) 尾数相乘 (Multiply Significands)

  • 将两个带隐含位的尾数(\(1.M_1\)\(1.M_2\))当作无符号整数送入乘法器(如我们之前学过的华莱士树乘法器 )。
  • 单精度中,24 位 \(\times\) 24 位会产生 48 位的中间乘积。

(3) 规格化 (Normalize)

  • 因为 \(1 \le 1.M < 2\),所以两数相乘的结果范围是 \(1 \le Product < 4\)
  • 因此,乘积的最高有效位要么在常规位(格式为 \(1.xxxx\)),要么向前进了一位(格式为 \(1x.xxxx\))。
  • 调整:如果是 \(1x.xxxx\),只需将结果右移 1 位,同时指数加 1即可。

(4) 舍入并确定符号 (Round and Sign)

  • 按照 IEEE 754 标准舍入。
  • 符号位直接通过异或逻辑门得出:\(\mathbf{S_{result} = S_1 \oplus S_2}\)

八、浮点运算的硬件精度:GRS 位 (Guard, Round, Sticky) 与舍入规则

8.1 GRS 位的定义与作用

在对齐(右移)或乘法运算中,尾数低位被移出后,如果直接丢弃,会造成极大的精度损失。为了硬件能够按照 IEEE 754 标准进行精确的舍入(Rounding to nearest even,向偶数舍入),硬件 ALU 在尾数运算时会向右多扩展 3 个附加位

  1. 保护位 (Guard bit, G):尾数右边第一位。对齐时被移出尾数的第一位数据会落入 G。
  2. 舍入位 (Round bit, R):G 右边的一位。被移出的第二位数据落入 R。
  3. 粘性位 (Sticky bit, S):R 右边的所有位。它的机制非常特殊:只要有任何一个 1 被移出到它的位置(或它的右边),S 就会被永久置为 1 并锁死;只有当移出的全都是 0 时,S 才为 0
    • 数学公式:一旦有 1 移入 \(S\),则 \(S = 1\)

可以把 G, R, S 三位组合看作是尾数最低有效位(LSB, Unit of Last Place)右侧的小数:

  • GRS = 0xx (即 \(G=0\)):代表丢弃的部分 \(< 0.5\) LSB(距离下舍入的值更近)。
  • GRS = 100:代表丢弃的部分 恰好等于 \(0.5\) LSB(处于两个可表示值的正中间,即 Tie 状态)。
  • GRS = 1xx (且 \(R\)\(S\) 至少有一个为 1):代表丢弃的部分 \(> 0.5\) LSB(距离上舍入的值更近)。

8.2 舍入规则 (Rounding Rules)

IEEE 754-2008 定义了 5 种舍入模式。在体系结构考试中,最常考也最复杂的是第一种(默认模式)

舍入模式 (Rounding Mode) 英文名称 核心规则与 GRS 行为
1. 偶数优先舍入 (默认) RNE (Round to Nearest, ties to Even) 舍入到最接近的值。如果是 Tie(\(GRS = 100\)),则舍入到尾数最低位(LSB)为 0 (偶数) 的那一边。
2. 四舍五入 (远离 0) RNA (Round to Nearest, ties away from zero) 舍入到最接近的值。如果是 Tie,一律向远离 0 的方向舍入(即绝对值变大)。
3. 向零舍入 (截断) RTZ (Round toward Zero / Truncate) 一律直接丢弃 G, R, S 保护位,尾数保持不变。
4. 向正无穷舍入 (朝天) RUP (Round toward \(+\infty\)) 若为正数且 \(GRS > 0\),尾数加 1;若为负数,直接截断(绝对值变小)。
5. 向负无穷舍入 (朝地) RDN (Round toward \(-\infty\)) 若为负数且 \(GRS > 0\),尾数加 1(绝对值变大);若为正数,直接截断。

8.3 RNE(Round to Nearest, ties to Even) 的处理流程

  • GRS < 100(即 \(G=0\)):直接截断(尾数不变)。
  • GRS > 100(即 \(G=1\)\(R \| S == 1\)):尾数加 1
  • GRS == 100(Tie,正好一半):
    • 若尾数最低位为 0(已经是偶数):直接截断(保持偶数)。
    • 若尾数最低位为 1(奇数):尾数加 1(使其变为偶数 0)。

8.4 运算示例

为了最清晰地展示这个过程,我们假设一个4 位有效尾数(1 位隐含的整数 + 3 位小数,即格式为 1.xxx)的简化浮点系统。

计算:\(1.000_2 \times 2^5 - 1.001_2 \times 2^1\)

第一步:对齐阶数 (Align Exponents)

为了进行减法,我们需要将阶数小的数据右移,使其阶数对齐到 \(2^5\)

  • 操作数 \(X = 1.000_2 \times 2^5\) (不需要移动)
  • 操作数 \(Y = 1.001_2 \times 2^1\) (需要右移 \(5 - 1 = 4\) 位)

右移过程中 G, R, S 位的变化追踪:

  • 初始 \(Y\) 尾数:1.001
  • 右移 1 位:0.100,此时被移出的 1 进入 \(G\) \(\rightarrow\) 0.100 | G=1, R=0, S=0
  • 右移 2 位:0.010,先前的 1 移入 \(R\) \(\rightarrow\) 0.010 | G=0, R=1, S=0
  • 右移 3 位:0.001,先前的 1 移入 \(S\) \(\rightarrow\) 0.001 | G=0, R=0, S=1
  • 右移 4 位:0.000,尾数末尾的另一个 1 移入 \(G\)。先前移入 \(S\)1 依然保存在 \(S\)
    最终对齐后的 \(Y\) 为:

    \[Y = 0.000_2 \times 2^5 \quad (\mathbf{G=1, R=0, S=1})\]

第二步:尾数减法 (Significand Subtraction)

我们带上 G, R, S 位进行高精度减法:

  X:  1.000 | 0 0 0  (GRS)
- Y:  0.000 | 1 0 1  (GRS)
-----------------------
结果: 0.111 | 0 1 1  (GRS)  <-- 结果为 0.111_011_2 * 2^5

第三步:规格化 (Renormalization)

因为结果为 0.111...,不满足 1.xxx 的规格化要求。我们需要将其左移 1 位,同时将阶数减 1。

  • 左移 1 位后,原来的 \(G\)0)被移入尾数的最低位。
  • 原来的 \(R\)1)移入 \(G\)\(S\)1)移入 \(R\)
  • 规格化后的中间结果为:

    \[\text{Fraction} = 1.110_2 \times 2^4 \quad (\mathbf{G=1, R=1, S=0})\]

    (注意:这就是为什么我们需要 R 和 S 位的深层次原因。左移后,原来的保护位补了上来,我们依然拥有高精度的舍入信息。)

第四步:执行 RNE(偶数优先)舍入

此时我们的尾数最后一行为 1.110,其保护位为 GRS = 110

  1. 评估 GRS:因为 \(G=1, R=1, S=0\),所以 \(GRS = 110\)
  2. 判断大小\(110 > 100\),说明丢弃的部分大于 \(0.5\) LSB(更靠近大数一侧)。
  3. 舍入操作:执行上舍入 (Round Up),将尾数最低位加 1:

    \[1.110 + 0.001 = 1.111\]

最终计算结果:

输出值存入寄存器:\(1.111_2 \times 2^4\)

Tie 状态(\(GRS = 100\))的“奇偶舍入”补充实例

如果上述减法算出来的最终中间结果是:

\[\text{Fraction} = 1.110_2 \times 2^4 \quad (\mathbf{G=1, R=0, S=0})\]

此时 \(GRS = 100\),处于 Tie 状态(正中间)。

  • 看尾数的 LSB:当前尾数 1.110 的最后一位是 0(偶数)。
  • 舍入决定:为了保持偶数,我们选择下舍入(截断)
  • 最终结果\(1.110 \times 2^4\)

而如果中间结果是:

\[\text{Fraction} = 1.111_2 \times 2^4 \quad (\mathbf{G=1, R=0, S=0})\]

此时同样是 \(GRS = 100\)

  • 看尾数的 LSB:当前尾数 1.111 的最后一位是 1(奇数)。
  • 舍入决定:为了使其变为偶数,我们选择上舍入(加 1)

    \[1.111 + 0.001 = 10.000\]

    此时产生进位,需要重新规格化:\(1.000 \times 2^5\)

这种向最接近的偶数舍入的机制,能够在连续的大量浮点数运算中,使得正向误差和负向误差相互抵消,从而将系统的整体统计偏差(Bias)降到最低。


九、RISC-V 浮点指令集架构 (RISC-V FP Architecture)

RISC-V 将浮点功能作为可选扩展(Extensions):

  • F 扩展:单精度浮点(32位)。
  • D 扩展:双精度浮点(64位)。

9.1 独立的浮点寄存器堆 (Separate FP Register File)

RISC-V 的浮点计算不使用整数寄存器(x0-x31),而是有一套完全独立的 32 个浮点寄存器:f0f31

  • 在 D 扩展中,每个寄存器宽 64 位(可以存放双精度);在 F 扩展中只使用低 32 位。
  • 注意f0 不是x0 那样硬连线为 0,它就是一个普通的浮点寄存器。

9.2 核心汇编指令分类

在 RISC-V 汇编中,浮点指令通常带有 .s (Single) 或 .d (Double) 后缀以指明数据类型。

(1) 浮点加载与存储 (Load & Store)
需要从内存加载到浮点寄存器,或反之(基址寻址的基址寄存器仍然是整数寄存器 rs1)。

  • flw fd, offset(rs1):Load Word (单精度)。
  • fsw fs2, offset(rs1):Store Word (单精度)。
  • fld / fsd:双精度加载/存储。

(2) 浮点算术运算 (Arithmetic)

  • fadd.s fd, fs1, fs2:浮点加法 fd = fs1 + fs2
  • fsub.s fd, fs1, fs2:浮点减法
  • fmul.s fd, fs1, fs2:浮点乘法
  • fdiv.s fd, fs1, fs2:浮点除法
  • fsqrt.s fd, fs1:计算平方根(微架构中常用牛顿迭代法或SRT硬件实现)。

(3) 浮点比较与分支 (Comparison & Branching)

RISC-V 的优雅设计:结果写回整数寄存器

与其他架构(如x86有专门的条件码寄存器)不同,RISC-V 的浮点比较指令会将布尔结果(1 表示 True,0 表示 False)写入整数寄存器 rd,然后程序可以使用普通的整数分支指令(如 beq, bne)来进行跳转。

  • feq.s rd, fs1, fs2:如果 fs1 == fs2,则整数寄存器 rd = 1,否则 rd = 0
  • flt.s rd, fs1, fs2:Less Than (小于)。
  • fle.s rd, fs1, fs2:Less or Equal (小于等于)。

代码示例:判断 f1f2 是否相等,如果不等则跳转到 Label

feq.s x5, f1, f2    # 如果 f1 == f2,将整数 1 存入 x5;否则存 0
beq   x5, x0, Label # 如果 x5 等于 0 (即比较结果为假),则跳转到 Label

(4) 浮点与整数的转换 (Conversion)
用于在浮点寄存器和整数寄存器之间转换数据格式。

  • fcvt.w.s rd, fs1:将单精度浮点数转换成 32 位整数(w 代表 word),写入整数寄存器 rd
  • fcvt.s.w fd, rs1:将 32 位整数转换成单精度浮点数。



Enjoy Reading This Article?

Here are some more articles you might like to read next: