简介:本资源是Behrooz Parhami教授经典著作《Computer Arithmetic》第二版(2010年牛津大学出版社出版)的完整高清PDF电子书,面向计算机体系结构、数字电路设计、嵌入式系统及高性能计算方向的高年级本科生、研究生与硬件工程师。全书系统覆盖数制转换、定点/浮点表示(含IEEE 754详解)、加减乘除与开方等核心算法,深入剖析ALU构造、流水线优化、并行运算硬件实现等关键设计原理,兼具理论深度与工程实践指导价值。资源为单文件PDF格式,共1个文件,大小3.9MB,内容完整、排版规范,含原书全部章节、习题、参考文献及作者致谢页,便于离线研读与重点标注。目前已有1694人学习下载,是理解CPU底层算术逻辑、开展FPGA数值加速器设计或备考体系结构类课程的重要权威参考资料。
1. 计算机运算:为什么一个加法器要花三十年才“定型”,而你写的a + b却从不报错?
你敲下a + b,编译器一闪而过,CPU 一拍即合——结果秒出。但你有没有想过:这个看似原子操作的加法,背后藏着三类完全不同的硬件实现路径?一类靠“猜”进位(超前进位),一类靠“堆”逻辑门(阵列加法器),还有一类干脆不等进位、边算边传(进位保存加法器)。这三种设计,不是演进关系,而是并存于同一颗现代 CPU 的不同模块中:ALU 用第一种,乘法器内部用第三种,而低功耗协处理器可能直接用第二种。Parhami 在《Computer Arithmetic》第二版开篇就点破:计算机算术不是“怎么算对”,而是“在面积、延迟、功耗、可测性四维约束下,如何让‘算对’这件事可预测、可复用、可验证”。这本书之所以被称作“经典巨作”,正因为它不教你怎么写a + b,而是带你拆开 ALU 的黑匣子,看清楚:为什么 IEEE 754 浮点除法必须容忍 0.5 ULP 误差?为什么冗余数制能让乘法器吞吐翻倍却不用改时序?为什么 CORDIC 算开方不用乘法器?这些不是玄学,是芯片工程师在硅片上用晶体管反复权衡后刻下的物理答案。搞懂它,你写的每一行数值计算代码,才真正有了“地基感”。
2. 数制与表示:为什么二进制补码不是唯一解,而冗余编码才是高性能的“后悔药”
2.1 补码的代价:溢出检测不是免费的,它是用额外逻辑换来的“确定性”
二进制补码(Two’s Complement)被广泛采用,不是因为它最自然,而是因为它让加减法电路能复用同一套硬件。但 Parhami 在第 2 章明确指出:补码的“简洁”是以牺牲中间过程的可观察性为代价的。例如,8 位补码加法0x7F + 0x01(127 + 1)结果为0x80(-128),表面看只是高位翻转,但硬件必须额外判断S = A ⊕ B ⊕ C_out(符号位异或进位)才能确认是否溢出。这个判断逻辑在关键路径上引入了至少两级门延迟。更隐蔽的问题是:当多个操作数并行相加(如乘法器中的部分积压缩)时,补码的符号扩展会强制插入大量全零/全一的“填充位”,白白占用布线资源和寄存器宽度。
提示:补码的溢出检测公式
V = C_in ⊕ C_out(进位输入异或进位输出)只适用于有符号加法;无符号加法溢出仅看C_out。混淆二者是初学者最常踩的坑。
2.2 冗余数制:用“多存一位”的空间换“少等一级”的时间
Parhami 在第 3 章系统性地引入了带符号数字(Signed-Digit, SD)表示法——这是理解现代高速乘法器的钥匙。SD 编码允许每位取值 {-1, 0, +1}(常记为1̄,0,1),关键特性是:任意两个 SD 数相加,进位最多传播一位。这意味着:加法器可以设计成“进位保存”(Carry-Save Adder, CSA)结构,把进位暂时存在下一级的“进位输入端”,而不必像补码加法器那样等进位链稳定。CSA 的延迟恒为 O(1),与位宽无关。
下面是一个 4 位 SD 加法的示意(1̄表示 -1):
A: 1̄ 1 0 1 (-1, +1, 0, +1) → 十进制值:-8 + 4 + 0 + 1 = -3 B: 0 1̄ 1 0 (0, -1, +1, 0) → 十进制值:0 - 4 + 2 + 0 = -2 ------------------- Sum: 1̄ 0 1 1 (-1, 0, +1, +1) → -8 + 0 + 2 + 1 = -5 ✅ Carry: 0 1 0 0 (进位向高位传递)注意:这里Sum和Carry是两组独立的位向量,它们的组合才代表真实数值。CSA 不做最终进位合并,而是把Sum和Carry一起送入下一级 CSA。直到最后一级,才用一个标准加法器(如超前进位)将两者合并。
这种“延迟解耦”正是 Wallace 树乘法器的核心:它把 n 个部分积(每个是 m 位 SD 数)用多级 CSA 压缩成两个向量,最后一步才合并。整个过程延迟为 O(log n),远优于逐级累加的 O(n)。
2.3 浮点表示的“契约精神”:IEEE 754 不是数学规范,而是工程妥协协议
第 17 章对 IEEE 754 的剖析直指本质:它定义的不是“正确答案”,而是“可复现的错误范围”。以单精度为例,1.0f + 1e-7f的结果在所有合规实现中必须落在[1.00000011920928955078125, 1.0000002384185791015625)区间内(即 ±0.5 ULP)。这个“误差许可带”是硬件设计者与软件开发者之间的契约——硬件不必追求无限精度,只需保证误差可控且可预测;软件则必须接受这个前提,不能假设a + b == b + a在浮点下恒成立(因舍入方向可能不同)。
Parhami 特别强调:规格化数(Normalized)、非规格化数(Denormal)、无穷大(Inf)、非数(NaN)这四类值,在硬件中对应四套完全独立的处理路径。例如,当检测到操作数为 Denormal 时,流水线必须插入微码序列来执行“左规”(Left-normalize),这会导致数十周期的停顿。现代 CPU 的“Denormals-are-zero”(DAZ)和“Flush-to-zero”(FTZ)模式,本质上就是用牺牲数学严谨性来换取确定性延迟。
2.4 常见问题排查:数制转换与表示陷阱的血泪经验
现象:用
printf("%f", 0.1f)输出0.100000001490116119384765625
原因:十进制0.1是无限循环二进制小数0.0001100110011...,单精度浮点只能存前 24 位有效位,尾数截断导致固有误差。
解决:对显示精度要求高的场景(如金融),改用定点数或十进制浮点(IEEE 754-2008 decimal32/64);若必须用二进制浮点,输出时指定小数位数(如%.1f)而非依赖默认精度。现象:C 语言中
int a = 0x80000000; printf("%d\n", -a);输出-2147483648(仍是负数)
原因:32 位补码中0x80000000是最小负数,其相反数+2147483648超出int表示范围,触发有符号溢出(未定义行为)。编译器可能优化为恒返回原值。
解决:使用无符号类型uint32_t进行位操作;或用llabs()等安全函数;在关键路径禁用-fwrapv编译选项。现象:FPGA 综合后,用 SD 编码的加法器时序不收敛,关键路径在进位链
原因:误将 SD 加法器的Sum和Carry输出直接连到下一级的A和B输入,忽略了 CSA 要求两路输入位宽严格匹配。若Carry向高位移了一位,就会造成错位加法。
解决:严格按 CSA 规范设计接口:Carry_out[i]必须连接到下一级A[i+1]或B[i+1];用generate块或参数化宏确保位宽对齐;在综合约束中显式设置set_false_path避免工具误判进位路径为关键路径。现象:启用 FTZ 模式后,某些科学计算结果精度骤降,甚至出现 NaN
原因:FTZ 将 Denormal 数强制置零,但若算法本身依赖 Denormal 的“渐进下溢”特性(如某些滤波器的极小系数),突然归零会破坏数值稳定性。
解决:在算法初始化阶段调用feenableexcept(FE_UNDERFLOW)捕获下溢异常,定位具体触发位置;改用双精度或增加输入缩放因子(scale-invariant design)。
3. 加法器设计:超前进位不是“更快的 Ripple”,而是用空间换确定性的范式革命
3.1 Ripple-Carry 的本质缺陷:它不是慢,而是“不可预测的慢”
教科书常把 Ripple-Carry Adder(RCA)描述为“简单但慢”,这掩盖了其真正的工程缺陷:RCA 的延迟是输入相关的(data-dependent)。当A=0xFF, B=0x01时,进位需从 bit0 逐级传播到 bit7,延迟为 8×t_gate;但当A=0x00, B=0x01时,只有 bit0 产生进位,延迟仅为 2×t_gate。这种波动性在时序分析中是灾难性的——静态时序分析(STA)必须按最坏情况(Worst-Case)约束,导致芯片不得不降频运行。Parhami 在第 6 章用门级仿真数据证明:在 65nm 工艺下,8 位 RCA 的 PVT(工艺-电压-温度)变化范围内,延迟波动可达 ±35%,而超前进位加法器(CLA)波动仅 ±8%。
3.2 CLA 的核心思想:把“进位生成”和“进位传播”抽象为布尔函数
CLA 的突破在于将进位链建模为递归函数。定义:
G_i = A_i & B_i(Generate:本位必然产生进位)P_i = A_i ^ B_i(Propagate:本位将输入进位传递出去)
则第 i 位的进位C_i = G_{i-1} | (P_{i-1} & C_{i-1})。展开后得:
C_1 = G_0 | (P_0 & C_0) C_2 = G_1 | (P_1 & G_0) | (P_1 & P_0 & C_0) C_3 = G_2 | (P_2 & G_1) | (P_2 & P_1 & G_0) | (P_2 & P_1 & P_0 & C_0)可见,C_i只依赖于低位的G_j、P_j和初始进位C_0,与中间进位无关。因此,只要预先计算所有G_i、P_i,就能用两级逻辑门(AND-OR 树)并行生成所有C_i。
下面是一个 4 位 CLA 的关键路径 Verilog 实现(简化版):
// 生成 G, P assign G[0] = a[0] & b[0]; assign P[0] = a[0] ^ b[0]; assign G[1] = a[1] & b[1]; assign P[1] = a[1] ^ b[1]; assign G[2] = a[2] & b[2]; assign P[2] = a[2] ^ b[2]; assign G[3] = a[3] & b[3]; assign P[3] = a[3] ^ b[3]; // 并行计算进位(C0 为 cin) assign c[1] = G[0] | (P[0] & c[0]); assign c[2] = G[1] | (P[1] & G[0]) | (P[1] & P[0] & c[0]); assign c[3] = G[2] | (P[2] & G[1]) | (P[2] & P[1] & G[0]) | (P[2] & P[1] & P[0] & c[0]); assign c[4] = G[3] | (P[3] & G[2]) | (P[3] & P[2] & G[1]) | (P[3] & P[2] & P[1] & G[0]) | (P[3] & P[2] & P[1] & P[0] & c[0]); // 求和 assign s[0] = a[0] ^ b[0] ^ c[0]; assign s[1] = a[1] ^ b[1] ^ c[1]; assign s[2] = a[2] ^ b[2] ^ c[2]; assign s[3] = a[3] ^ b[3] ^ c[3];这段代码的关键在于:c[4]的计算深度仅为 3 级门(AND-OR-OR),而同等位宽的 RCA 需要 8 级。但代价是门数爆炸——4 位 CLA 约需 30 个门,而 RCA 仅需 16 个。Parhami 指出:CLA 的价值不在“绝对速度”,而在“延迟确定性”。现代 CPU 的 ALU 采用分段 CLA(如 4-4-4-4 结构),每段内用 CLA,段间用快速进位链,平衡面积与性能。
3.3 进位选择加法器(Carry-Select):用“预计算”消灭关键路径
当 CLA 的门数增长难以承受时,Carry-Select Adder(CSLA)提供另一条路:对每个分段,同时计算C_in=0和C_in=1两种情况下的结果,再用实际进位选择输出。例如,将 16 位分为 4 段,每段 4 位,则每段需两个 4 位加法器(一个设cin=0,一个设cin=1),再加一个 2:1 多路选择器。总延迟 = 4 位加法器延迟 + 多路选择器延迟,与位宽无关。
Parhami 在第 7 章给出关键数据:在 28nm 工艺下,16 位 CSLA 比 CLA 面积小 22%,延迟仅高 15%,但 PVT 波动降低至 ±5%。这解释了为何 ARM Cortex-A 系列的整数 ALU 大量采用 CSLA——它用可预测的面积开销,换来了最严苛的时序收敛保障。
3.4 常见问题排查:加法器时序与功能验证的硬核细节
现象:CLA 综合后,
c[4]信号在仿真中出现毛刺(glitch),导致下游逻辑误触发
原因:c[4]的布尔表达式含多个 AND 项 OR 在一起,各路径延迟不一致,导致瞬时竞争。例如P[3]&G[2]比P[3]&P[2]&P[1]&P[0]&c[0]快,OR 门在慢路径到达前先输出 0,后又跳变。
解决:在关键进位输出后插入一级寄存器(pipeline register);或改用“门控时钟”技术,仅在进位稳定后采样;在 RTL 中用assign #1 c[4] = ...添加保守延迟(仅用于仿真)。现象:CSLA 的多路选择器输出延迟超标,成为新关键路径
原因:误用标准单元库中的通用 2:1 MUX,其驱动能力不足。当选择信号sel来自远端寄存器时,sel的翻转延迟叠加 MUX 本身延迟,超过预算。
解决:选用高驱动能力的 MUX 单元(如MUX2X1D2);将sel信号提前一级寄存;或重构为“选择进位”而非“选择结果”——即只对进位链做选择,求和仍用原cin。现象:在 FPGA 上实现 32 位 CLA,时序报告提示“无法满足 1GHz 时序”
原因:FPGA 的 LUT(查找表)实现复杂布尔函数效率低下。c[31]的表达式含 32 项 AND-OR,需多级 LUT 级联,布线延迟占主导。
解决:改用 FPGA 厂商原语(如 Xilinx 的CARRY4),其内部用专用进位链硬连线;或降级为分段 CLA(如 8 段 × 4 位),用CARRY4构建每段。现象:验证环境发现,当
A=B=0x7FFFFFFF(最大正数)时,加法器输出s=0xFFFFFFFE,但c_out=0(应为 1)
原因:测试平台未覆盖C_in=1场景。该输入下,A+B+C_in实际为0x7FFFFFFF + 0x7FFFFFFF + 1 = 0x1FFFFFFFE,结果截断为 32 位0xFFFFFFFE,c_out应为最高位进位1。
解决:编写边界测试向量(corner-case testbench),强制遍历C_in ∈ {0,1}和A,B ∈ {0, max, min, -1};用形式验证工具(如 JasperGold)证明c_out == (A+B+C_in) >> N。
4. 乘法器架构:为什么现代 CPU 的乘法器不用“移位相加”,而用“树形压缩”
4.1 移位相加的幻觉:它在软件里高效,在硬件里是面积黑洞
教科书常以“手算乘法”类比硬件乘法器,但这严重误导。软件中a * b的移位相加(Shift-Add)只需 O(N) 时间和 O(1) 空间;但硬件实现时,若用 N 个 1 位乘法器(AND 门)生成 N 个部分积,再用 N-1 个加法器串行累加,面积为 O(N²),延迟为 O(N²)。Parhami 在第 10 章用数据说话:32 位移位相加乘法器需约 2000 个门,而同等性能的 Wallace 树仅需 1200 个门,延迟从 64 级降至 12 级。
4.2 Wallace 树:用 CSA 的“并行性”打破加法器的串行瓶颈
Wallace 树的核心洞察是:乘法的本质是“部分积的二维矩阵求和”,而 CSA 允许我们沿列方向并行压缩。以 4×4 乘法为例:
a3 a2 a1 a0 ← 被乘数 × b3 b2 b1 b0 ← 乘数 ---------------- p0: a0b0 ← 第 0 行(最低位) p1: a1b0 a0b1 p2: a2b0 a1b1 a0b2 p3: a3b0 a2b1 a1b2 a0b3 p4: a3b1 a2b2 a1b3 p5: a3b2 a2b3 p6: a3b3Wallace 树将此矩阵按列分组,每列用 CSA 将 3 个以上位压缩为 2 个位(Sum + Carry),再将 Carry 移到下一列。重复此过程,直到只剩两行,最后用 CLA 合并。
下面是一个 4 位 Wallace 树的压缩步骤(简化):
Column 0: [a0b0] → Sum0, Carry0→col1 Column 1: [a1b0, a0b1] → Sum1, Carry1→col2 Column 2: [a2b0, a1b1, a0b2] → CSA → S2, C2→col3 Column 3: [a3b0, a2b1, a1b2, a0b3] → 两轮 CSA → S3, C3→col4 ... 最终得到两行:[S0,S1,S2,...] 和 [C0,C1,C2,...]Parhami 强调:Wallace 树的延迟取决于“列中最大位数”,而非总位数。对于 N×N 乘法,列中最多有 N 个位,CSA 每轮将位数减至 ⌈2k/3⌉,故总轮数为 log₃/₂(N) ≈ 1.71 log₂(N)。这正是其 O(log N) 延迟的来源。
4.3 Dadda 乘法器:用“最小化 CSA 轮数”实现面积最优
Dadda 乘法器(第 11 章)是 Wallace 的精炼版。它不追求每列都立即压缩到 2 位,而是预先计算每列的目标位数(基于斐波那契数列),只在必要时才插入 CSA。例如,目标序列设为[1,2,3,4,6,9,13,...],若某列当前位数为 5,而目标为 4,则不压缩;若为 7,目标为 6,则压缩一轮。这减少了 CSA 总数,面积比 Wallace 小 10–15%,延迟略增 5–10%。
Parhami 给出实测对比(65nm ASIC):
| 指标 | Wallace (32×32) | Dadda (32×32) |
|---|---|---|
| 门数 | 12400 | 10600 |
| 关键路径延迟 | 1.8 ns | 1.95 ns |
| 功耗 | 1.2 mW | 1.05 mW |
这印证了他的核心观点:没有“最好”的乘法器,只有“最适合场景”的乘法器。GPU 偏爱 Wallace(极致吞吐),而 IoT MCU 倾向 Dadda(极致能效)。
4.4 常见问题排查:乘法器验证与综合的致命细节
现象:Wallace 树综合后,某列 CSA 的
Carry输出连接到错误列,导致结果错乱
原因:手动编写位连接时,误将Carry_out[i]连到col[i]而非col[i+1]。CSA 的 Carry 是向高位进位,必须右移一位。
解决:用参数化生成器(如 Python 脚本)自动推导连接关系;在 RTL 中用assign carry_col[i+1] = csa_carry[i];显式声明位移。现象:Dadda 乘法器在仿真中通过,但 FPGA 下电后功能异常
原因:Dadda 的压缩策略依赖精确的位宽对齐,而 FPGA 综合器对未初始化的wire可能赋予 X 值。当某列 CSA 输入含 X 时,Sum和Carry输出也为 X,污染整个树。
解决:所有中间信号强制初始化(wire [3:0] sum = 4'b0000;);在顶层添加initial begin #1 $display("Dadda init OK"); end验证初始化时序。现象:验证覆盖率报告显示,“部分积全零”场景未覆盖
原因:测试激励未包含a=0或b=0。此时所有部分积为 0,CSA 的Sum和Carry全为 0,但硬件可能因未驱动而悬空。
解决:在 testbench 中显式添加a=0,b=0,a=0,b=1,a=1,b=0四组向量;用断言assert (a==0 || b==0) |-> (out==0);捕获。现象:启用综合工具的“面积优化”后,乘法器时序崩溃
原因:工具将 CSA 的Sum和Carry输出合并到同一寄存器组,破坏了 Wallace 树的“延迟解耦”设计意图。
解决:在综合约束中添加set_dont_touch [get_cells -hier -filter "name=~*csa*"]锁定 CSA 模块;或改用“混合优化”策略,对乘法器区域设set_max_delay -ignore_clock_latency 1.0 [get_ports out]。
5. 浮点除法与开方:为什么牛顿迭代不是“数学技巧”,而是硬件友好的收敛协议
5.1 SRT 除法:用“查表+反馈”替代“试商”,把除法变成状态机
传统长除法(Restoring Division)需对每一位尝试2^i × divisor并回退,硬件实现复杂。SRT(Sweeney, Robertson, Tocher)算法(第 14 章)将其重构为:每步根据当前余数R_i的高位比特,查表决定商位q_i ∈ {-2,-1,0,1,2},然后更新R_{i+1} = 2*R_i - q_i*D。关键优势是:q_i的选择只依赖R_i的有限高位(如 3–4 位),无需精确计算2*R_i / D。
Parhami 展示了一个 4 位 SRT 除法器的状态转移表(radix-4):
| R_i[3:0] (2's comp) | q_i | 注释 |
|---|---|---|
| 1000 – 1111 | -2 | 余数为负且绝对值大 |
| 0100 – 0111 | +1 | 余数为正且适中 |
| 0000 – 0011 | 0 | 余数接近零 |
| 1100 – 1111 | -1 | 余数为负但绝对值小 |
这个表被固化在 ROM 中,每周期只需一次查表+一次移位+一次加法。现代 x86 的 FP divide 单元即基于 SRT,其延迟固定为 10–20 周期(与操作数无关),而恢复除法延迟波动达 30–60 周期。
5.2 牛顿-拉夫逊迭代:用“二次收敛”把除法变成乘法
SRT 解决了“怎么除”,牛顿法(第 15 章)解决了“怎么快”。其核心是:求1/D的倒数,再用N × (1/D)得商。迭代公式为:
x_{k+1} = x_k × (2 - D × x_k)若初始猜测x_0满足|1 - D×x_0| < 0.5,则每次迭代精度翻倍(quadratic convergence)。Parhami 证明:对 IEEE 754 单精度,只需 2 次迭代即可达到 24 位精度;双精度需 3 次。
硬件实现的关键是:迭代中的D × x_k是乘法,而现代 CPU 的乘法器已高度优化。因此,牛顿法将“慢除法”转化为“快乘法+快加法”的组合。ARM Cortex-A77 的 FP divider 即采用此方案,吞吐率比 SRT 高 3 倍。
5.3 CORDIC:不用乘法器的开方器,是旋转坐标系的几何直觉
CORDIC(Coordinate Rotation Digital Computer)算法(第 22 章)彻底颠覆了“开方必须用乘法”的认知。其思想是:将开方√x转化为向量(x,0)绕原点旋转,使其 y 分量归零,此时 x 分量即为√x。旋转角度预设为θ_i = arctan(2^{-i}),每次旋转仅需加减和右移(y_{i+1} = y_i ∓ x_i>>i,x_{i+1} = x_i ± y_i>>i)。
下面是一个 8 位 CORDIC 开方的伪代码:
def sqrt_cordic(x): x_i, y_i = x, 0.0 for i in range(8): # 8 iterations if y_i < 0: x_i, y_i = x_i + (y_i >> i), y_i - (x_i >> i) else: x_i, y_i = x_i - (y_i >> i), y_i + (x_i >> i) return x_i * K # K = 0.607252935... is scaling constantParhami 指出:CORDIC 的硬件代价仅为 8 个加法器和 8 个移位器,面积不足传统 Newton 开方器的 1/3。它被广泛用于 FPGA 的实时信号处理,如雷达脉冲压缩。
5.4 常见问题排查:浮点除法与开方的隐性陷阱
现象:牛顿迭代计算
1/3.0f,2 次迭代后结果为0.3333333432674407958984375,但期望0.333333313465118408203125(IEEE 754 单精度精确值)
原因:初始猜测x_0的精度不足。若x_0仅 12 位精度,第一次迭代后误差仍达 1e-4,第二次无法收敛到 24 位。
解决:用 LUT 存储 256 个1/D的粗略值(8-bit index),作为x_0;或用x_0 = 0.5 * (3 - D)(Goldberg 公式)提升初始精度。现象:CORDIC 开方器在
x=0时输出 NaN
原因:算法假设x>0,当x=0时,y_i始终为 0,if y_i < 0永假,导致x_i不更新。
解决:在顶层添加if (x == 0) return 0;的特例处理;或修改迭代逻辑,当|y_i| < ε时提前退出。现象:SRT 除法器在
D=0时死锁,状态机卡在某态
原因:除零未被检测,查表模块收到非法输入(如R_i=0且D=0),输出无效q_i,导致R_{i+1}不收敛。
解决:在除法器前端添加divisor_zero检测器,一旦D==0立即置status=DIV_BY_ZERO并终止;用assert property (@(posedge clk) (d != 0) || (status != BUSY));形式验证。现象:浮点开方结果在跨平台(x86 vs ARM)上不一致
原因:不同架构对sqrt指令的实现不同:x86 用 Newton,ARM 用 Goldschmidt,初始猜测和迭代次数策略不同,导致 ULP 差异。
解决:在跨平台应用中,禁用硬件sqrt,改用软件实现(如math.h的sqrtf);或接受 IEEE 754 允许的 ±1 ULP 差异,不将其视为 bug。
6. 设计取舍:为什么我每次写数值
本文还有配套的精品资源,点击获取