Skip to content
NotesCSAPP

程序的机器级表示、算术运算与系统基础知识回顾。

第03章 计算机的算术运算

整数运算

加法与减法

  • 溢出 (Overflow) 条件
    • 加法:当两个同号操作数相加,结果超出可表示范围时发生溢出。
      • 两个正数相加,若结果符号为 1,则溢出。
      • 两个负数相加,若结果符号为 0,则溢出。
      • 一正一负相加,绝不会发生溢出。
    • 减法:当两个异号操作数相减时可能发生溢出(同号相减无溢出)。
      • 正数减负数,若结果符号为 1,则溢出。
      • 负数减正数,若结果符号为 0,则溢出。
  • 多媒体算术运算
    • 图形和媒体处理通常需要对8位和16位的向量数据进行操作。
    • 采用具有分段式进位链的64位加法器,可同时对8个8位、4个16位或2个32位向量进行运算。
    • 此技术被称为 单指令多数据 (SIMD, Single-Instruction, Multiple-Data)子字并行 (Subword Parallelism)
    • 饱和操作 (Saturation):发生溢出时,结果保持为可表示的最大(或最小)值,而非像二进制补码那样进行取模回绕(常用于音频限幅、视频饱和处理)。

乘法

  • 硬件实现与优化
    • 基础实现:被乘数左移,乘数右移,使用128位ALU和128位乘积寄存器。
    • 优化实现:使用64位ALU,被乘数固定,乘积寄存器整体右移(乘法/移位并行执行,每次部分积相加占1个周期)。
    • 高速实现:使用多个加法器并行执行并支持流水线化,权衡性价比。
  • RISC-V 整数乘法指令集
指令用途结果说明溢出检查方法
mul乘法返回乘积的低64位结合高位指令检查
mulh有符号乘积高位返回乘积的高64位(双有符号数)mul 结合检查64位溢出
mulhu无符号乘积高位返回乘积的高64位(双无符号数)mul 结合检查64位溢出
mulhsu混合乘积高位返回乘积的高64位(一有符号,一无符号)结合高位指令检查

除法

  • 算法与硬件实现
    • 需要首先检查除数是否为 0
    • 除法恢复 (Restoring Division):先做减法,若余数小于0,则将除数加回以恢复原值。
    • 有符号除法:使用绝对值相除,最后按规则调整商和余数的符号。
    • 基础与优化硬件:基础版除数右移、商左移;优化版使用64位ALU,余数寄存器左移,可与乘法器共享硬件资源。
    • 高速实现:除法无法像乘法那样完全并行(减法依赖余数符号),更快的除法器(如 SRT 除法)每步可产生多个商位。
  • RISC-V 整数除法指令集
指令用途异常处理机制
div有符号除法溢出和除0 不产生错误,只返回预定义结果以保证执行速度
rem有符号求余数同上
divu无符号除法同上
remu无符号求余数同上

浮点运算与 IEEE 754 标准

浮点数表示方法

  • 基础格式:类似于科学记数法的二进制表示 ±1.xxxxxxx2×2yyyy
  • IEEE 754 标准:目前几乎全球通用,解决了科学计算代码的可移植性问题,分为单精度(C语言 float)和双精度(C语言 double)。
  • 编码结构
    • S (Sign):符号位(0 代表非负数,1 代表负数)。
    • Fraction (有效数字/尾数):规格化表示为 1.0|有效数字|<2.0,小数点前隐含一位 1,无需显式存储(即 隐含位 (Hidden Bit))。
    • Exponent (指数):采用 偏置表述 (Biased Notation),实际指数 = 寄存器指数 - 偏阶。
精度类型总位数偏阶 (Bias)有效数字精度近似十进制精度最小正值范围最大正值范围
单精度32位12723位尾数 (223)~6位十进制数~ 1.2×1038~ 3.4×1038
双精度64位102352位尾数 (252)~16位十进制数~ 2.2×10308~ 1.8×10308

特殊数值表述

  • 非规格化数 (Denormalized Numbers):指数全为 0,此时隐含位也是 0,允许渐进下溢以减少精度损失(注:此时 0.0 有两种正负表述)。
  • 无穷大 (Infinity):指数全为 1,尾数全为 0,表示 ±,可用于后续运算避免溢出检查。
  • NaN (Not-a-Number, 非数字):指数全为 1,尾数不全为 0,表示非法或未定义结果(如 0.0 / 0.0),可传递至后续运算。

浮点加法运算步骤

  • Step 1. 对齐小数点:将指数较小的数向右移位,直到其指数与较大的数相同。
  • Step 2. 尾数相加:将对齐后的有效数字进行加法运算。
  • Step 3. 规格化与检查:规格化相加结果,并检查是否发生上溢 (Overflow) 或下溢 (Underflow)。
  • Step 4. 舍入:对结果进行舍入处理,若舍入后破坏了规格化,则再次进行规格化。
  • 硬件特性:浮点加法器远比整数加法器复杂,需要多个时钟周期完成,通常支持流水线化。

算术精确性与舍入控制

  • 附加保护位:硬件运算时保留多余的位以提升精度,包括 保护位 (Guard)舍入位 (Round)粘性位 (Sticky)
  • 舍入模式 (IEEE 754-2008 定义 5 种):舍入到最近的偶数、舍入到最近的最大幅值、向0舍入、向 舍入、向 + 舍入。
  • 精度重要性:浮点精度不仅对科学计算至关重要,也影响消费市场(如著名的 Intel Pentium FDIV 硬件漏洞)。

RISC-V 浮点指令与代码映射

寄存器与核心指令

  • 寄存器约定:拥有独立的32个浮点寄存器 f0f31。双精度占用整个64位,单精度数值保存在低32位中。浮点和整数操作之间严格分离。
  • 指令汇总
操作类型单精度指令 (.s)双精度指令 (.d)说明
内存存取flw, fswfld, fsd单精度取/存字,双精度取/存双字
算术运算fadd.s, fsub.s, fmul.s, fdiv.s, fsqrt.sfadd.d, fsub.d, fmul.d, fdiv.d, fsqrt.d浮点加/减/乘/除/平方根运算
逻辑比较feq.s, flt.s, fle.sfeq.d, flt.d, fle.d比较结果为0或1,保存在整数目的寄存器中,供 beq/bne 分支跳转使用

汇编代码映射示例

示例 1:将华氏度转换为摄氏度

  • C 代码:
c
float f2c (float fahr) {
    return ((5.0/9.0)*(fahr - 32.0));
}
  • RISC-V 汇编映射 (假定参数在 f10 中,返回结果在 f10 中):
assembly
f2c:
    flw     f0, const5(x3)      # f0 = 5.0f
    flw     f1, const9(x3)      # f1 = 9.0f
    fdiv.s  f0, f0, f1          # f0 = 5.0f / 9.0f
    flw     f1, const32(x3)     # f1 = 32.0f
    fsub.s  f10, f10, f1        # f10 = fahr - 32.0f
    fmul.s  f10, f0, f10        # f10 = (5.0f/9.0f)*(fahr-32.0f)
    jalr    x0, 0(x1)           # return

示例 2:双精度矩阵乘法 (部分循环逻辑)

  • C 代码 (32x32 矩阵 $C = C + A \times B$):
c
void mm (double c[][], double a[][], double b[][]) {
    size_t i, j, k;
    for (i = 0; i < 32; i = i + 1)
        for (j = 0; j < 32; j = j + 1)
            for (k = 0; k < 32; k = k + 1)
                c[i][j] = c[i][j] + a[i][k] * b[k][j];
}
  • RISC-V 汇编映射 (最内层循环片段,假定指针在 x10~x12,索引在 x5~x7):
assembly
L3: 
    # 计算并加载 b[k][j]
    slli    x29, x7, 5          # x29 = k * 32 (行大小)
    add     x29, x29, x6        # x29 = k * 32 + j
    slli    x29, x29, 3         # x29 = 字节偏移 (乘8)
    add     x29, x12, x29       # x29 = b[k][j] 的地址
    fld     f1, 0(x29)          # f1 = b[k][j]

    # 计算并加载 a[i][k]
    slli    x29, x5, 5          # x29 = i * 32 (行大小)
    add     x29, x29, x7        # x29 = i * 32 + k
    slli    x29, x29, 3         # x29 = 字节偏移 (乘8)
    add     x29, x11, x29       # x29 = a[i][k] 的地址
    fld     f2, 0(x29)          # f2 = a[i][k]

    # 核心浮点运算
    fmul.d  f1, f2, f1          # f1 = a[i][k] * b[k][j]
    fadd.d  f0, f0, f1          # f0 = c[i][j] + a[i][k] * b[k][j]
    # (注:此处可用 fmadd.d 单条指令替换 fmul.d 和 fadd.d)

    # 循环控制
    addi    x7, x7, 1           # k = k + 1
    bltu    x7, x28, L3         # if (k < 32) go to L3
    fsd     f0, 0(x30)          # 保存回 c[i][j]