ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

计算机算术核心:从浮点数舍入到硬件实现与验证

计算机算术核心:从浮点数舍入到硬件实现与验证 简介《计算机运算》第二版是Behrooz Parhami教授关于计算机算术算法与硬件设计的经典著作适合计算机科学、电子工程专业学生及硬件设计、嵌入式系统工程师研读。全书系统覆盖数的表示与进制转换、IEEE 754浮点格式、补码加减运算及溢出处理、乘法与除法器硬件实现并延伸至平方根迭代算法、ALU构造、流水线与并行处理等高级主题兼顾理论基础与工程实践。资源为PDF格式共1个文件压缩包大小3.9MB内容为原版电子书便于检索、标注与反复查阅。已吸引1674人学习下载足见其在计算机体系结构领域的参考价值。通过本书读者可系统掌握计算机算术底层原理与硬件设计思路为深入理解CPU运算单元、优化数值计算性能打下扎实基础。1. computer arithmetic 开篇从浮点数的“不精确”说起写 C 或 Rust 做数值计算的人迟早会撞上一个反直觉现象0.1 0.2在 double 里并不等于0.3。这不是语言实现的问题而是二进制浮点表示在底层带来的约束。IEEE 754 标准只是这整套规则里最容易被感知的一层真正困难的在于浮点运算单元里加法器如何传播进位、除法器如何迭代余数、舍入发生在哪一步、下溢时硬件该做什么。这一整块内容体系结构领域习惯统称为 computer arithmetic。网上能搜到、也被从业者当作经典参考的第二版《Computer Arithmetic》核心价值就是把表示、运算算法与硬件映射这条线完整串起来。本文顺着这条线从舍入规则出发讲到加法器、乘法器、SRT 除法与平方根迭代最后落回一套可执行的验证方法把理论和落地路径一次讲透。2. computer arithmetic 的表示层补码、舍入模式与下溢边界2.1 补码溢出与同一加法器完成加减法computer arithmetic 不只在讲浮点整数运算同样属于这个范畴。任何处理器都要同时支持整数加减乘除与移位而这些运算基于定点二进制表示。补码表示有一个很值得注意的特性最高位的权值是负的。对 n 位补码范围是-2^(n-1)到2^(n-1)-1而无符号数范围是0到2^n-1。两者范围不同但加减法共用一个加法器区别只在于是否有符号扩展和溢出判断。下表以 8 位为例给出边界表示方式最小值最大值典型使用场景无符号数0255地址、计数器、位掩码补码-128127有符号整数运算二进制原码-127127少数 DSP 定点格式偏置码-128127IEEE 754 指数部分补码的优势在于“减法是加补码”这意味着一套加法器电路就能同时完成加减法。溢出判断规则很直接两个正数相加得到负数或两个负数相加得到正数即发生溢出。具体到电路上只要看最高位进位C_out与次高位进位C_n-1是否不同也就是V C_out XOR C_n-1。这个表达式在综合工具里很常见但也容易被随手写错尤其是做有符号与无符号混合运算时。实际工程中更稳妥的做法是在 RTL 里显式区分输入是否有符号而不是靠综合器自动推断。提示补码的-2^(n-1)没有对应的正数这是取绝对值时需要单独处理的边界情况。很多数值库在计算abs(INT_MIN)时踩坑根就在这。2.2 IEEE 754 舍入模式RNE 是默认值但不是唯一选择浮点数不是无限精度的。两个浮点数做乘法时尾数位宽会翻倍结果必须舍入回目标格式。IEEE 754 定义了四种舍入模式就近偶舍入RNE、就近远离零舍入RNA、向零舍入RZ、向上舍入RP、向下舍入RM。其中 RNE 是默认模式它的规则是“结果恰好落在两个可表示数中间时选尾数为偶数的那一个”。这个细节常被忽视但它直接影响数值稳定性也为后面判断硬件实现是否符合规范提供了抓手。四种模式的具体行为差异如下舍入模式正值示例负值示例实现倾向RNE 就近偶2.5 - 2-2.5 - -2大多数 CPU ALU 默认RNA 就近远离零2.5 - 3-2.5 - -3需要额外判定逻辑RZ 向零截断2.5 - 2-2.5 - -2实现最简单DSP 常用RP 向上2.1 - 3-2.1 - -2区间运算中常用RM 向下2.1 - 2-2.1 - -3区间运算中常用要用足够直观的方式验证舍入行为可以用 Python 的struct把单精度浮点拆成位模式直接观察尾数低位的变化import struct def f2bits(x: float) - str: # 单精度转 32 位无符号整数再按符号/指数/尾数切分 b struct.unpack(I, struct.pack(f, x))[0] sign (b 31) 0x1 exp (b 23) 0xFF frac b 0x7FFFFF return fsign{sign} exp{exp:#010b} frac{frac:023b} for v in [1.5, 2.5, 3.5, 0.1, 0.2, 0.3]: print(f{v:5.1f} - {f2bits(v)})这段代码里struct.pack(f, x)先把 Python float 截成单精度struct.unpack再解析成一个整数最后用位运算切分三个字段。运行后能清楚看到0.1在二进制里并不是 0.1而是0x3DCCCCCD这个近似值0.3则是0x3E99999A。这两个近似值相加后再舍入和0.3的比特位存在偏差于是 0.10.2 不等于 0.3 的根源就被定位到位模式层面了。2.3 下溢与亚正常数不可忽略的边界层浮点格式里指数全 1 表示无穷大与 NaN指数全 0 则用来表示零和亚正常数。亚正常数的作用是让绝对值小于最小正规数的数值仍然具备一定的精度。正规数的最小正数是2^-126在它之下如果没有亚正常数半步就会直接跳到 0运算结果会产生一个很大的相对误差。引入亚正常数之后下溢路径变成“先渐进到亚正常数再变成零”代价是硬件需要额外处理这种非规格化输入。C 标准提供fpclassify宏来区分这些类别在写底层数值库时很实用#include stdio.h #include math.h int main(void) { double vals[] {0.0, -0.0, 1e-310, 1e-320, INFINITY, NAN}; for (int i 0; i 6; i) { int cls fpclassify(vals[i]); // 依次判断正规数、亚正常数、零、无穷、NaN if (cls FP_NORMAL) printf(normal\n); else if (cls FP_SUBNORMAL) printf(subnormal\n); else if (cls FP_ZERO) printf(zero\n); else if (cls FP_INFINITE) printf(infinite\n); else if (cls FP_NAN) printf(nan\n); } return 0; }这段代码的要点是1e-310在大多数实现中是亚正常数1e-320可能已经是 0运行后不同架构的结果会略有差异正好能用来确认目标平台对下溢的处理是否符合 IEEE 754 的渐进下溢要求。实际项目里如果某段算法频繁产生亚正常数性能会显著下降因为很多硬件遇到亚正常数会走慢速路径。真正要区分“精度足够但没有性能”还是“性能拉满但精度不足”就要在定义浮点格式时把亚正常数的开关策略想清楚。3. 从加法器到乘法器computer arithmetic 的运算单元怎么搭3.1 进位链决定的性能行波进位与超前进位加法器是计算机算术最基本的运算单元所有复杂运算最终都可以化归为若干次加法。最朴素的行波进位加法器把每一位的进位串行传递n 位加法的关键路径长度是 O(n)。在这个结构里高位的和必须等低位的进位稳定下来才能计算组合逻辑延迟随位宽线性增长。对于 32 位或 64 位运算这个延迟在高速处理器里是不可接受的。解决办法是让“进位生成”和“进位传播”信号并行推导。定义两个信号g_i a_i b_i表示第 i 位一定产生进位p_i a_i ^ b_i表示第 i 位会传播低位进位。那么进位可以写成展开式module cla4( input [3:0] a, b, input cin, output [3:0] s, output cout ); wire [3:0] g a b; // 进位生成 wire [3:0] p a ^ b; // 进位传播 wire [3:0] c; assign c[0] cin; assign c[1] g[0] | (p[0] c[0]); assign c[2] g[1] | (p[1] c[1]); assign c[3] g[2] | (p[2] c[2]); assign cout g[3] | (p[3] c[3]); assign s p ^ c; // 和位 传播位异或进位 endmodule这段代码里g、p是组合信号每位进位不再等待前一位完成而是由底层输入直接算出来。c[1]只依赖g[0]、p[0]和cinc[3]虽然表达式更长但同样只依赖原始输入因此逻辑深度基本固定。实际芯片不会直接展开到 64 位全超前进位因为扇出太大常见的做法是 4 位或 8 位一组做 CLA组间再用行波或更高一级 CLA 连接。工程上这叫“组内超前进位、组间行波”是一个面积和延时的折中。这里给出一组参考数据加法器类型典型位宽关键路径量级面积代价行波进位 RCA32O(n)最小超前进位 CLA32O(log n)中等进位选择 CSLA32O(sqrt(n))较大进位跳跃 CSkA32O(sqrt(n))中等选择依据不只看延迟还要看布局布线约束。对 FPGA 来说LUT 结构本身自带快速进位链直接用行波进位综合出来的结果往往比手写 CLA 更快因为 FPGA 的专用进位路径是硬核资源。对 ASIC 设计则要老老实实做逻辑综合和时序分析。手写加法器前先看一下目标工艺和综合策略否则可能做了无用功。3.2 Booth 重编码减少部分积才是乘法器面积的关键乘法器的直接实现方式是把被乘数左移若干位后求和每一行部分积对应乘数的一位。n 位乘 n 位会产生 n 个部分积求和树的面积和布线随之增大。Booth 重编码的思路是与其让乘数逐位决定“加被乘数”还是“加 0”不如把连续的多位一起看用一次移位或取负来等价表示。最常用的是基 4 Booth 编码每次看乘数的三位(y_{2i1}, y_{2i}, y_{2i-1})产生一个系数{-2, -1, 0, 1, 2}于是部分积数量从 n 个降到 n/2 个。系数 2 可以用左移一位实现系数 -2 则先取反再加 1硬件上只多一个符号扩展逻辑。看一个最小化的例子假设寄存位宽被限制则每轮迭代表达式为// 基4 Booth编码器输入相邻3位输出控制信号 module booth_enc( input [2:0] y, // y[1:0] 当前位, y[2] 低位 output reg [2:0] sel // 0: 0, 1: x, 2: 2x, 3: -x, 4: -2x ); always (*) begin case (y) 3b000, 3b111: sel 3d0; 3b001, 3b010: sel 3d1; 3b011: sel 3d2; 3b100: sel 3d4; // -2x 3b101, 3b110: sel 3d3; // -x endcase end endmodule这个编码器的输入y是乘数相邻三位输出sel决定当前部分积是被乘数、两倍被乘数还是它们的负数。实现里还要处理符号扩展负数部分积必须把符号位扩展到乘法结果全宽再求和否则部分积相加时会出错。这是很多人第一次写 Booth 乘法器时最容易漏掉的地方。漏符号扩展的典型症状是正数乘法完全正确一旦乘数是负数结果低位正确但高位多出全 1。定位方法很简单把中间部分积打印出来对照《Computer Arithmetic》里关于符号扩展的推导检查即可。3.3 乘法结果截断与粘滞位精度控制在硬件层收口浮点乘法器尾数部分通常把 n 位乘 n 位结果保留 2n 位再舍入回 n 位。舍入时需要知道三个信息保留位、舍入位、粘滞位。保留位是结果中将被丢弃部分的最高位舍入位是它的下一位粘滞位则是指示被丢弃部分是否含任何非零位。粘滞位的计算很直接所有保留位以下的位做逻辑或。如果没有粘滞位往“偶尾数”舍入时就无法判断精确中间值结果会偏随机导致同一套输入在不同实现里产生不同结果。硬件中用粘滞位压缩电路把这些低位归并成一个位能显著减少加法树宽度。这条规则在编写浮点仿真模型时同样适用先用高精度累加结果再手动按保留位、舍入位、粘滞位实现 RNE 逻辑对比硬件 RTL 输出的位模式可以快速定位实现错误。4. 除法与平方根computer arithmetic 里最难啃的迭代算法4.1 SRT 除法用冗余余数换掉进位传播等待除法在处理器里一向是出故障最多的模块。直接试商法需要计算减去除数的余数并判断符号每步都要等进位链稳定延迟与位宽成正比这在现代处理器中不可接受。SD 有余数法也就是 SRT 除法核心技巧是允许余数有冗余也就是不要求每次迭代的余数严格落在某个小区间里而是把它放在一个冗余范围里下一步可以纠偏。这样每次迭代可以用少量位来查表决定商位避开全宽加法器的进位传播等待。基 4 SRT 除法的商位选取用部分余数和除数的高位查表。典型的查找表规模在 4KB 到 16KB 之间具体行数取决于冗余度和部分余数的位数。常用参数大致如下参数名称典型取值说明基数 r4每次迭代产生 2 个二进制商位商位集{-2,-1,0,1,2}冗余表示允许选择非精确商位部分余数位数6~8参与查表的高位截断除数位数3~4只需除数前几位参与查表商选择表行数2^7 到 2^9每行对应一组余数高位区间实现时有个关键约束商选择表必须保证选择后的余数满足|R_next| (D * rho)其中 rho 是冗余系数。如果查找表太小或截断太狠会出现“错误的商位但再也无法恢复”的情况这在测试中表现为随机输入偶发错误结果。定位这类问题最直接的方法是把算法级模型里每次迭代的余数区间打印出来对比 RTL 仿真的部分余数第一处超出边界的位置就是出错根源。4.2 平方根迭代与除法同源的递推平方根本质上和除法共享同一套减去除法的框架差别在于除数变成了不断扩展的根值。每次迭代当前根 Q 会更新为Q_next Q q * r^-j余数递推式与除法高度相似只是减去的项带上了 2Q 这个因子。硬件实现通常直接复用除法器的大部分数据通路只需额外增加一个“根值拼接寄存器”。软件层面的模拟可以用逐位求根法下面是一个简单的基 2 迭代框架def isqrt_fixed(n: int, bits: int 16) - int: # 用定点整数模拟逐位平方根每次试根1位判断余数是否非负 root 0 rem 0 for i in range(bits - 1, -1, -1): rem (rem 2) | ((n (2 * i)) 3) cand (root 2) | 1 # 试商位为 1 时的候选值 if rem cand: rem - cand root (root 1) | 1 # 该位确认为 1 else: root root 1 # 该位确认为 0 return root这段代码每轮把余数左移两位取被开方数高两位拼入得到当前余数再与候选值比较。它演示的是平方根除法算法家族里“试商”的基本形态。参数bits控制被开方数的位宽输出root是整数平方根。逐位法可以验证正确性但性能不高实际软浮点环境更常用牛顿迭代下一节展开。4.3 软件除法与牛顿迭代没有硬件除法器时的兜底路径在缺少硬件除法器的嵌入式平台或软浮点环境里Newton-Raphson 迭代是常见替代方案。它把除法a/b转化为求1/b的根x迭代公式x_{n1} x_n * (2 - b*x_n)每次迭代精度翻倍。初值通常用一个小的查找表或浮点近似指令获得迭代一到三次就能达到 double 精度double soft_div(double a, double b) { // 初值用 1/b 的粗略近似实际工程里应查一个 8 或 16 项的表 double x0 1.0 / b; // 仅演示收敛路径不是最终结果 // 第一轮牛顿迭代修正相对误差 double x1 x0 * (2.0 - b * x0); // 第二轮迭代后精度接近 double 上限 double x2 x1 * (2.0 - b * x1); return a * x2; }这段代码里x0直接用除法得到一个近似值工程实现应换成查表或位操作来避免递归依赖硬件除法。每轮迭代包含两次乘法和一次减法没有除法指令依赖所以能在只支持乘法浮点运算的核上运行。要注意的是牛顿迭代无法直接处理除数为 0、结果为 NaN 或无穷的这些分支调用前必须检查b的类别否则异常会顺着乘法链被“修正”成错误的有限值。另一个常见误区是一上来就迭代三次实际上 double 的 52 位尾数只要初值精度达到 13 位左右三轮迭代后误差就低于 ULP再加轮次只会引入舍入噪声。5. 验证一套 computer arithmetic 实现边界向量、黄金参考与异常标志5.1 关键边界向量与预期行为验证浮点实现的第一件事不是跑随机数而是构造一组能触发特殊路径的边界向量。IEEE 754 里最容易出错的输入都集中在零、无穷、NaN 与指数边界附近。我一般会先用下面这张表做冒烟测试输入组合预期行为对应检查点0.0 0.00.0符号位处理-0.0 -0.0-0.0零的符号规则inf -infNaN异常输入检测最大正规数 最大正规数inf溢出标志最小亚正常数 / 20 或渐进下溢下溢标志NaN 参与任一运算NaNNaN 传播规则0.0 * infNaN无效操作标志5.2 用黄金参考向量做逐位回放边界冒烟通过后还要验证普通数值路径的逐位一致性。常见做法是用 Python 或 C 的高精度参考实现生成一段测试向量然后把 DUT 输出按位比对。比对的不是 double 值是否相等而是最终输出与参考结果的位模式完全一致因为 IEEE 754 要求运算结果必须像先精确计算再舍入一样。生成向量的一种简单方法是使用线性同余随机数发生器保证每次运行序列一致python3 gen_ref.py 100000 ref.txt ./dut_runner ref.txt | sha256sum sha256sum ref_expected.txt这里gen_ref.py输出的是每个用例的原始输入和参考位模式dut_runner读取同样输入并把 DUT 结果以十六进制写出。两条 sha256 值一致说明整条路径的舍入行为没有偏差。有一点要提醒不要在测试用例里用memcmp直接比较double的二进制表示除非你已经预先确定了 NaN 的负载位格式。NaN 的尾数位在不少处理器上会被修改比较时应该先掩掉尾数低位或者只比较符号位与指数位。5.3 浮点异常标志验证不可只对数值有些错误只有数值正确性但异常标志错误这种情况在边界用例中很常见比如溢出结果是对的但 FE_OVERFLOW 没有置位。抓这类问题的标准做法是读写浮点环境#include fenv.h #include stdio.h int main(void) { feclearexcept(FE_ALL_EXCEPT); double r 1e308 * 10.0; // 应当触发溢出 int raised fetestexcept(FE_OVERFLOW | FE_INEXACT); if (raised FE_OVERFLOW) printf(overflow raised\n); if (raised FE_INEXACT) printf(inexact raised\n); printf(result: %f\n, r); return 0; }编译运行用gcc -stdc11 fenv_test.c -lm ./a.out。feclearexcept先清空所有标志fetestexcept再检查指定标志位。这套接口在 RTL 验证里对应异常输出端口的断言把“结果值”和“标志位”分开检查才能在算法级模型里尽早发现硬件控制逻辑的错误。验证到这一步一套 computer arithmetic 实现的正确性边界才算被完整覆盖过一遍。本文还有配套的精品资源点击获取
返回列表