Hacker News 中文摘要

RSS订阅

The Intel 8087 chip: floating-point addition and subtraction

1980年,英特尔(Intel)推出英特尔8087(Intel 8087)芯片,让国际商业机器公司(IBM)的IBM个人计算机(IBM PC)等系统的浮点运算大幅加快。8087虽支持正切、平方根和对数等高级运算,重点却在加法与减法。相比8086处理器,加法几乎快100倍,复杂运算提升更多。为解释浮点加减法及无穷大等特殊值如何实现,我用凿子打开芯片并借显微镜观察:中央的微码ROM含1648条微指令,下半部分数据通路对80位值执行浮点计算。

正文

1980 年,英特尔推出 8087 芯片,让 IBM PC 等系统的浮点运算大幅提速。尽管 8087 支持正切、平方根、对数等高级运算,但这里只关注基础算术:加法和减法。相比 8086 处理器,8087 的加法速度快了近 100 倍,复杂运算提升更多。下面通过 8087 的微码和电路,说明浮点加减法的具体实现,包括无穷大等各种特殊值的处理。

为了研究 8087 的电路,我用凿子打开了一块芯片的盖子,用显微镜拍出下面的高分辨率图像。微码 ROM 是芯片中央的大块矩形区域,存放着控制芯片的 1648 条微指令。芯片下半部分(红框)是数据通路,即对 80 位数值执行浮点计算的电路。

8087 数据通路特写,显示用于加法和减法的功能模块。点击可查看大图。

放大数据通路可以看到加减法所需的功能模块。核心部件是位于中央的整数加法器,它从 B 寄存器接收一个操作数,另一个操作数来自临时寄存器或堆栈寄存器等部件,结果存入和寄存器(sum register)。移位器可以对数值左移或右移任意位,它在芯片上占用很大面积,因为电路支持快速移位。浮点运算大量使用移位器来对齐数值;前导零电路统计数值左侧的零位或零字节数量,以便把它们移走。指数转换器处理指数,检测上溢和下溢。这些专用电路帮助 8087 实现了高性能。

多数计算机用整数作为数字的基本表示,但很多应用需要带小数部分的数,例如 1.23。而且整数范围很有限:32 位整数大约只能表示 -20 亿到 20 亿。为解决这些问题,计算机通常实现浮点数,在极大范围内提供小数。

浮点数通常表示为 符号 × 有效数 × 2 指数。有效数是形如 1.bbb… 的二进制数:开头一个 1,接着二进制小数点(类似十进制小数点)及其余位。二进制小数部分的位是 2 的负幂:2^-1、2^-2 等。所以二进制 1.1 等于 1 + 1/2,即十进制 1.5;二进制 1.0011 等于 1 + 1/8 + 1/16,即 1.1875。指数让数值突破有效数的有限范围,例如二进制 1.1 × 2^8 是 384。指数更大时,浮点数覆盖范围极大,即便 32 位短浮点数也能覆盖超过 10^-36 到 10^38。

浮点加减法比整数运算复杂。相加(或相减)前必须对齐二进制小数点,就像十进制数要对齐小数点一样。运算后还要调整结果的指数,把有效数规格化,使其以 1 开头。

一次简单浮点数加法的步骤。

上图演示了一次简单的浮点加法:13.5+3.5。前两行是两个操作数表示为指数和有效数。两个操作数都已规格化,最左位是 1。相加前,第二个操作数必须右移两位来对齐,使指数相等(本例指数差为 2)。随后对已对齐的有效数做整数加法。本例相加产生进位,即左侧多出一个位。结果必须右移一位规格化,并把指数加 1。

浮点减法类似,但更复杂。结果可以是正、负或零,也可能比操作数小几个数量级。下图是基本减法 8-7.5。和加法一样,第二个操作数右移使指数匹配(即二进制小数点对齐)。两个有效数用整数减法相减。结果通过左移规格化,使最高位为 1。与加法不同,规格化可能需要很大的移位;本例左移四位,指数减 4 得到结果。

一次简单的浮点数减法。

减法的一个复杂之处在于结果可能为负。这时把有效数变为正,并翻转结果的符号。减法也可能得到零,此时无法规格化,而是根据情况返回特殊值 +0 或 -0。

8087 的相关硬件细节

本节解释 8087 的一些特性,它们在加减法微码实现中起重要作用。8087 支持多种数据类型,但内部用 80 位“临时实数”(temporary real)格式存数。临时实数包含 1 位符号、15 位指数和 64 位有效数。15 位指数允许大到超过 10^4932 的荒谬数值,64 位有效数支持极高精度的数。指数存储时加了 16383 的偏置,因此存储指数总是正数,即使真实指数为负。例如指数 1 存为 0x4000,指数 -16 存为 0x3fef。

80 位临时实数格式与寄存器格式。圆点表示二进制小数点,类似于十进制小数点。

8087 有八个内部寄存器,组织成堆栈。程序员用这些寄存器给 8087 编程,每个寄存器存一个 80 位浮点数。为优化性能,寄存器堆栈中每个数值还带有标签:zero、valid、special 或 empty。零标记为 zero;普通浮点数值标记为 valid;若是无穷大、非数(NaN)或非规格化值,则标记为 special;empty 表示寄存器没有存值,用于检测堆栈下溢(读空寄存器)或上溢(存入非空寄存器)。

8087 还有内部使用的临时寄存器。tmpA 和 tmpB 是 80 位寄存器,各带两个标签位。tmpC 不同:它没有符号、指数或标签,有效数为 68 位;tmpC 多一位防止溢出,还有三个低位用于舍入,称为 guard、round 和 sticky。

微码中,每条 16 位微指令执行一个底层操作。许多操作在寄存器之间搬运数据,其他操作提供条件跳转、子程序调用和返回。

8087 处理的核心是 69 位整数加法器(我之前讨论过)。加法器超过 64 位,是因为有三个舍入位和几个用于溢出与乘法的高位。整数加法需要两条微指令:第一条指定相加的第一个操作数,并选择进位、舍入等选项(减法时有一个选项对第二个操作数取补)。加法器的第二个操作数总是来自 B 寄存器,注意它与 tmpB 无关。相加结果放入特殊的和寄存器。第二条微指令(可能隔很多条指令)把结果从和寄存器送到目的地。

浮点运算还大量使用位移位器,它对 68 位数值左移或右移任意位(我此前写过)。和加法器一样,移位操作也需两条微指令:第一条把数值送入移位器并配置方向和位数,第二条把结果从移位器送到目的地。

8087 的加减法微码

8087 几乎所有操作都用微码实现。基本数学运算(加、减、乘、除)先执行同一段微码(十进制地址 #0001),把两个操作数送入临时寄存器 tmpA 和 tmpB。操作数通常来自堆栈寄存器,也可来自内存,但这里不讨论内存情况。

接着,微码通过微子程序调用处理具体操作类型。8087 微码引擎有一个微型 ROM,存放跳转或子程序调用的目标地址表。目标 0 是特殊的,它为乘法、除法或加/减法准备了三个不同地址(由 ROM 中的特殊电路实现。设计芯片时,如果想做这种奇怪的优化,是可以的)。

加减法微子程序从地址 #0232 开始,微码如下。控制流很复杂,所以我在注释里放了流程图。微码先处理 0+0 或 0-0 的零情况,然后相减两个操作数的指数。如果第一个操作数指数较小(结果为负),就交换操作数,使指数较大的操作数在前。(8087 用“符号锁存器”跟踪结果符号;如果交换操作数会改变符号,则在 #0244 翻转锁存器。)

0232 jmp #0236 if not tmpA:tag ZERO 检查 0+0

0233 jmp #0236 if not tmpB:tag ZERO

0234 return if not unsigned sub 0+0 同号

0235 jmp #0263 处理 0+0 异号

0236 tmpA sign -> sign latch 非零:tmpA 符号送入符号锁存器

0237 tmpB:exp -> Breg

0238 adder: tmpA:exp - Breg cin=1 比较指数

0239 jmp #0245 if adder pos

0240 sumreg:frac -> Breg 若 tmpB 指数更大则交换

0241 adder: zero - Breg cin=1

0242 xchg tmp

0243 jmp #0245 if not unsigned sub

0244 !sign latch -> sign latch 若交换影响符号则翻转符号

0245 xchg tmp 继续

0246 shift zero R 0 bytes, 0 bits 零

0247 call CREATE_DENORM 移位 tmpB 以匹配指数

0248 xchg tmp

0249 zero -> tmpB:exp 把指数调整存入 tmpB:exp

0250 jmp #0269 if not unsigned sub 是基础加法吗?

0251 adder: tmpA:frac - Breg cin=1, roundmode tmpA - tmpB

0252 jmp #0256 if adder pos

0253 sumreg:frac,rnd -> Breg/rnd 有效数取负

0254 adder: zero - Breg cin=1, roundmode

0255 !sign latch -> sign latch 翻转符号

0256 jmp #0275 if Sum register is zero 若结果为零则跳转

0257 shift R -> tmpB:frac 原 tmpB,来自 CREATE_DENORM

0258 jmp #0282 if tmpA top bit tmpA 非非规格化?

0259 jmp #0282 if tmpB:frac[63] tmpB 非非规格化?

0260 jmp #0289 若两者都非规格化则跳过规格化

0261 tmpA:exp -> expConv 处理零

0262 jmp #0264 if not tmpA top bit

0263 zero -> tmpA 处理零情况

0264 0 -> tmpA sign +0

0265 zero -> tmpA:frac

0266 return if not round chop

0267 1 -> tmpA sign 返回 -0

0268 return

0269 adder: tmpA:frac + Breg cin=0, roundmode 基础加法:tmpA + tmpB

0270 jmp #0289 if not sumreg[64] 检查溢出

0271 shift 1 sumreg:sign,frac,rnd R 右移 1 位规格化

0272 shift R -> sumreg:frac,sign,rnd

0273 expAdj++ 增加指数调整

0274 jmp #0289 结束加法

0275 shift sumreg:frac,rnd L 0 bytes, 3 bits 零?移位获取舍入位

0276 shift L -> sumreg:frac

0277 jmp #0261 if Sum register is zero 若无舍入位则跳转

0278 zero -> sumreg:frac 清零结果

0279 jmp #0289 if not tmpA top bit 非规格化?

0280 expAdj-- 若非规格化,向下调整指数

0281 1 -> sumreg:frac,rnd 设置结果最低位

0282 lzcount -> shiftcountH 前导零调整:处理字节

0283 shift sumreg:frac,rnd L count byte sticky

0284 shift L -> sumreg:frac,rnd

0285 lzcount -> shiftcountL 处理剩余前导零位

0286 shift sumreg:frac,rnd L count bit sticky

0287 shift L -> sumreg:frac,rnd

0288 shiftcount -> tmpB:exp 记住位移位数

0289 sign latch -> tmpA sign 存储符号

0290 call ADJUST_PRECISION if not FBLD/FBSTP 必要时调整精度

0291 sumreg:frac -> tmpA:frac 存储调整后的小数部分

0292 tmpB:exp -> Breg get exp

0293 adder: tmpA:exp - Breg cin=0 adjust by Breg

0294 expAdj -> Breg

0295 adder: sumreg:frac + Breg cin=0 adjust by expAdj

0296 sumreg:frac -> expConv exp in expConv to check for overflow, etc

0297 sumreg:frac -> tmpA:exp store exp in tmpA

0298 return 完成

在地址 #0247,重要的对齐步骤发生:tmpB 被移位,使其二进制小数点与 tmpA 对齐。这由微码子程序 CREATE_DENORM 完成。移位 tmpB 的位数由和寄存器传入;此前减法已算出两个指数的差值(若差值为负,则已被取补,因此传入的是正值)。

通常结果的指数与(较大)操作数的指数相同,但并非总是如此。指数变化在两处跟踪。首先,一个称为指数调整(expAdj)的特殊两位寄存器保存因左移或右移一位带来的调整;硬件把 expAdj 初始化为 1,以便有空间递减。更大的指数变化记录在 tmpB 寄存器的指数字段;微码在 #0249 把它初始化为 0。

在地址 #0250,微码做出关键决定:底层操作是无符号加法还是无符号减法?根据涉及的符号,高层操作(加法或减法)可能与底层操作不匹配。例如 2+3、-2-3、-2+(-3) 等,两个无符号数相加,结果为 5(正或负)。而 -2+3、2-3、3-2 等,两个无符号数相减,结果为 1(正或负)。我用“无符号加法”指前一种情况,即无符号数值相加;“无符号减法”指后一种情况,即无符号数值相减。关键是加法指令可能导致无符号加法或减法,减法指令也一样。

8087 有专门电路判断底层无符号操作是加还是减,可用条件测试。tmpA 和 tmpB 的符号位负数为 1、正数为 0。若把 1 分配给减法、0 分配给加法,则 tmpAsign⊕tmpBsign⊕op 在无符号操作为减法时得 1,为加法时得 0。8087 还有条件测试符号相同还是不同。

无符号加法

无符号加法路径较简单,从 #0269 开始。步骤和前面描述基本一致:对齐数值、相加、规格化。首先,较小的数在 tmpB 中右移,直到指数匹配。然后做(无符号)加法。如果最高位有进位,结果在 #0271 右移。因为结果右移一位,特殊的两位 expAdj 计数器递增,以跟踪所需的指数调整。

在 #0289,结果符号用符号寄存器中的值更新。8087 支持三种大小:80 位临时实数、64 位实数和 32 位实数。对较小的尺寸,微码子程序 ADJUST_PRECISION 把数值裁剪到合适大小(这并不像听起来那么简单,因为结果向上舍入可能需要调整指数,记录在 expAdj 寄存器中)。最后,结果的指数由初始指数(在 tmpA 中)计算:减去 tmpB 指数中的调整值(通常为 0),加上 expAdj 寄存器中的调整值,再减 1(因为 expAdj 从 1 开始)。指数存入结果,同时也存入“指数转换器”——一个评估指数以检测上溢或下溢的电路。

此时,加减法微子程序返回公共微码。公共微码检查是否发生上溢或下溢,即数值无法放入浮点数。这种情况下会调用 NONNORMAL_RESULT 子程序处理,可能触发中断、返回无穷大或返回零。(详见我之前关于非正常结果的文章。)

随后,结果通过写入相应堆栈寄存器被压入堆栈。如果原指令指定结束时弹出堆栈,则堆栈顶标记为空,并更新堆栈指针。最后,“运行下一条指令”微指令表示当前指令微码结束。浮点指令至此完成。

无符号减法

现在看无符号减法路径,从 #0251 开始。(记住指令可能是加法,例如 -2 + 3。)微码用 64 位加法器执行无符号减法:tmpA-tmpB;第二个操作数取负以完成减法。结果可正可负,取决于哪个值更大。其余路径与加法相同:设置符号、调整精度、计算最终指数。

如果减法结果为零,#0275 测试三个舍入位。若舍入位被置位,则把结果的最低位置位,并把指数调整减 1 来抵消(对于非规格化数,结果被清零,因为没有“额外”空间提升舍入位)。若结果真为零,有 #0261 处的特殊路径,因为微码需要确定结果是 +0 还是 -0。0-0 也会汇入这段代码,以处理 +0 和/或 -0 相加或相减的各种可能。通常正负零的行为符合你对“偏向”某一侧的零的预期:例如 ⁺0 + ⁺0 是 ⁺0,⁻0 - ⁺0 = ⁻0。但如果零相互抵消呢?例如 ⁺0 + ⁻0 或 ⁻0 - ⁻0?在这种情况下,向下舍入得到 ⁻0,其他舍入模式得到 ⁺0。

前导零电路统计前导零的数量,以决定移多少位。为控制电路规模,前导零移位分两步:先按前导零字节数左移结果;接着前导零电路统计剩余的前导零位数(此时为 0 到 7),再按该位数左移。总移位量存入 tmpB 的指数。

无符号减法路径在 #0289 与前述无符号加法路径合并,调整精度、计算指数并存储结果。随后微子程序返回同样处理乘法和除法的公共代码。

公共微码中的特殊情况处理

8087 处理无穷大、零、非数、非规格化数等许多特殊情况。8087 设计者仔细考虑了这些特殊情况,并试图用数学上合理的方式处理。下表只是 8087 处理的特殊情况的一部分。

《用户手册》中描述的部分无穷大特殊情况处理方式。

虽然这些特殊情况让 8087 在数学上更准确,却让微码复杂得多。下面的微码是加法、减法、乘法和除法共用的。它按上述方式加载操作数并存储结果,但主要处理特殊情况。这段微码是“面条代码”,连续 18 个条件分支来处理大量特殊情况。这里做高层描述,注释里的流程图提供细节。

0001 call MEM_OP if mem op 也许从内存取操作数

0002 jmp #0004 if mem op

0003 st(i) -> tmpB 从堆栈取

0004 jmp #0018 if DE (denormalized) interrupt 若非规格化中断则退出

0005 st(0) -> tmpA 栈顶送 tmpA

0006 jmp #0008 if not reverse bit 若非 FSUBR、FMUL、FDIVR 则不跳?

0007 xchg tmp 反转操作数

0008 jmp #0019 if tmp anomalous 处理异常值

0009 call INSTR_SWITCH 按指令类型切换:J1(乘)、J2(加/减)、J3(除)

0010 call NONNORMAL_RESULT if not exp normal 返回并完成指令

0011 jmp #0014 if dst st(i) 测试目的地位

0012 tmpA -> st(0) 目的地 st(0)

0013 RNI

0014 tmpA -> st(i) 退出:目的地 st(i)

0015 jmp #0018 if not pop at end 若结束时不弹出则 RNI

0016 empty -> SP(0) tag 标记栈顶为空

0017 stackPtr++ 弹出

0018 RNI

0019 call SPECIAL_TMPS 处理特殊值

0020 jmp #0023 if not flag

0021 jmp #0011 if expConv[0] 跳到结束

0022 jmp #0009 按指令类型切换

0023 cond3b -> sign latch 更多条件

0024 jmp #0039 if expConv[0]

0025 jmp #0032 if not tmpB:tag ZERO

0026 jmp #0035 if tmpA:tag ZERO

0027 except:zero div

0028 NaN -> tmpA NaN,结束

0029 high bit -> tmpA:frac

0030 sign latch -> tmpA sign

0031 jmp #0037 结束

0032 jmp #0035 if not tmpA:tag ZERO 除以非规格化/0

0033 tmpB:frac -> sumreg:frac 除以 0

0034 jmp #0055 if not Sum register is zero

0035 except:invalid

0036 NaN -> tmpA

0037 jmp #0018 if intr 跳到 RNI

0038 jmp #0011 结束

0039 jmp #0051 if not tmpA:tag SPECIAL 更多条件

0040 jmp #0046 if not tmpB:tag SPECIAL

0041 jmp #0028 if FMUL

0042 jmp #0035 if FDIV FDIVR

0043 jmp #0035 if not affine ∞ ∞×∞:测试是否仿射

0044 jmp #0035 if unsigned sub 测试是 ∞ 加还是减

0045 jmp #0038

0046 jmp #0049 if not FMUL

0047 jmp #0035 if tmpB:tag ZERO

0048 jmp #0028

0049 jmp #0028 if FDIV FDIVR

0050 jmp #0038

0051 jmp #0054 if not FMUL

0052 jmp #0035 if tmpA:tag ZERO 除以 0?

0053 jmp #0028

0054 jmp #0057 if not FDIV FDIVR

0055 zero -> tmpA 0÷非规格化 或 n÷∞

0056 jmp #0030 做处理并结束

0057 xchg tmp

0058 jmp #0061 if sign mismatch 若符号不匹配

0059 jmp #0062 if unsigned sub 测试是加还是减

0060 jmp #0038 结束

0061 jmp #0038 if unsigned sub 结束

0062 tmpA sign -> sign latch 符号取反

0063 !sign latch -> tmpA sign

0064 jmp #0038 结束

看 #0008,8087 微架构包含一个条件测试,检查任一临时寄存器是否为空或带有特殊值标签。若是,则调用我称为 SPECIAL_TMPS 的子程序。它检查两个临时寄存器,并设置两个标志,指示需要哪种特殊处理。

如果两个标志都被置位,说明某个操作数被标记为空,通常表示堆栈下溢,此时微码返回非数(NaN)。如果第二个标志清零,说明某个操作数是接近零的非规格化数,此时算术操作继续。

其余代码路径很复杂,例如根据操作数符号、具体算术操作、或操作数是零还是无穷大来分支。有八条代码路径返回非数:n÷非规格化、0÷0、0÷伪零、∞×0、∞÷∞、0×∞、∞ op ∞ proj、以及 ∞-∞ affine。多数很直观——例如 0×∞ 没有有意义的值——但最后两个需要多解释几句。

8087 有两种无穷大模式,由控制寄存器中的一位选择。“仿射闭包”(affine closure)下无穷大带符号,+∞ 和 -∞ 是两个不同值。推荐模式是“射影闭包”(projective closure),无穷大无符号。在仿射模式下,∞+∞ 完全合法,得到正无穷大。但 ∞-∞ 没有意义;回答 0 会是任意的,因此返回 NaN。在射影模式下,对无穷大的任何操作都没有意义,因为无穷大既无限正又无限负。

微码有八条返回无穷大的路径:n÷0、∞×n、∞×∞、n×∞、∞÷x、x±∞、∞±x 和 ∞+∞ affine。(第一种情况假设除零异常已被屏蔽。)前五种情况,微码通过组合操作数的符号生成无穷大的符号。但后三种情况从无穷大操作数取符号,忽略另一个符号。换句话说,乘除可以翻转无穷大的符号,加减则不能。

两条路径处理特殊的 0 结果:0÷非规格化 和 n÷∞。微码根据操作数符号生成带正确符号的 0。

前导零电路

减法中,数字可能相消,留下需要移位去掉的前导零。8087 的移位器能对 64 位数值移任意位。前导零电路的任务是统计前导零,以决定移位量,即统计第一个 1 位左边有多少个零,并把它变成二进制值。这基本等同于对第一个 1 位的位置进行编码的优先编码器。

前导零电路有些复杂,因为每个二进制输出位都依赖所有输入位;64 位检测器有 64 位输入。一种方法是列出真值表并优化,得到一堆逻辑;另一种是用分层方法,即 log(N) 级的树。8087 采用更简单的办法:第一遍按字节消除前导零,第二遍消除剩下 0 到 7 个前导零位。每一遍都需要一个独立的 8 输入前导零检测电路。这种方法较慢,但电路简单得多:两个 8 位前导零检测器,而不是一个 64 位前导零检测器。

下面示意图展示把 0 到 7 个前导零转换成 3 位二进制值的电路。为简化示意图,我把第一层逻辑替换成表示 8 位输入的位掩码。例如 x01xxxxx 表示若第二位为 0、第三位为 1、其余位任意,则信号为高。我加了文字说明;由于优化,这些说明有一些例外。例如 x01xxxxx 匹配任何有两个前导零的模式,也匹配一些没有前导零的模式,但那些不影响结果。关键是它匹配所有有 2 个前导零的情况,且不匹配有 1 个或 3 个前导零的情况。

检测 0 到 7 个前导零位的电路。检测前导零字节也采用相同方法。

电路输出是一个 3 位二进制值,表示前导零的数量。顶部 NOR 门生成低位,有 1、3、5 或 7 个前导零时置位。高位由底部 NOR 门生成:有 4、5、6 或 7 个前导零时置位。中间位较复杂,多一层逻辑判断是否有 4 或 5 个前导零(或可能是 0 或 1)。两个 1 情况阻止四个零,而 00 对意味着四个前导零会变成至少 6 个。(这是粗略解释,但可以枚举 256 种情况验证它们都产生正确答案。)

要统计带前导零的字节数,使用类似逻辑。不过每组 8 位先经过 OR 门,检测该字节是否有任何位置位。于是逻辑判断存在多少个前导零字节。

在芯片上,前导零逻辑紧邻和寄存器左侧,检查和寄存器的每一位。

结论

多数人不会多想计算机的加减法操作。但背后有有趣的算法和特殊情况,这些特性体现在巧妙的硅电路里。处理正负零、正负无穷大和非数的复杂规则已成为现代计算机的一部分,因为 8087 引出了今天使用的 IEEE-754 浮点标准。不过 8087 在特殊情况下有些过头。8087 的一些特性,如射影无穷大、unnormals 和伪零,因为不值得费力而从标准中被去掉。

注释与参考

  1. 临时实数格式与外部格式的一个区别是:临时实数中开头的 1 被显式存储,而外部格式中开头的 1 是隐式的、不存储,从而多一位精度,但让算术更复杂。

  2. 下面的流程图显示了加减法微码的控制流。(点击查看大图。)

加法 / 减法微码的流程图。

  1. William Kahan 是 8087 背后的浮点专家。他写过各种设计决定的动机,若想了解细节,例如为什么 NaN != NaN,可以参考他的文章。(一条有趣的评论讨论了 0^0 的值,网上常有争论。Kahan 说:“越来越多的人一致认为 x**0 = 1 对所有 x 成立,包括 0、∞ 和 NaN。”)

  2. 下面的流程图显示了基本算术指令共用的微码。该流程图把上面的流程图作为子程序调用。众多特殊情况路径已编号。

基本算术操作通用微码的流程图。(点击查看大图。)

  1. SPECIAL_TMPS 的操作总结如下。它通过 flag 触发器和指数转换器寄存器(expConv)返回状态。其操作是:

| 输入 | 结果 | flag | expConv | | --- | --- | --- | --- | | empty | NaN,异常 | 1 | 1 | | NaN | (较大的)NaN,异常 | 1 | 1 | | infinity | 无穷大 | 0 | 1 | | denorm | unnorm | 1 | 0 | | divide-by-zero(ish) | 不变 | 0 | 0 |

我称为“divide-by-zero(ish)”的行表示除法指令试图除以零或接近零的 unnormal 数。下面补充 8087 如何表示 0 和接近 0 的值的背景。普通浮点数的尾数首位为 1。不幸的是,普通数在最小的普通数和零之间有巨大空隙。非规格化数打破这条“规则”,允许高位为零,从而在 0 附近有更多数。(用十进制类比:假设从 1.003、1.002、1.001、1.000 往下数。如果最高位必须保持为 1,下一个可能的数就是 0,相比 1.000 是巨大跳跃。非规格化数让你继续小步走:0.999、0.998、……、0.001、0。)

对于非规格化数,指数取最小可能值。unnormals 与非规格化数类似,只是指数值更大。unnormals 的目的是:如果非规格化异常被屏蔽,非规格化数就被转换成 unnormal 并继续执行。

对于真正的零值,8087 使用带符号零,因此可以有 +0 或 -0。真零用最小可能指数存储。但 8087 也支持“伪零”,即指数较大的零值。伪零可能来自两个极小的 unnormal 值相乘。(到了 80387 浮点芯片,英特尔去掉了 unnormals 和伪零。)

SPECIAL_TMPS 子程序的微码我之前详细讨论过,这里不再赘述。

评论

作者发文后询问接下来该剖析 8087 的哪个部分,社区回应以致敬与怀旧为主,气氛友好、几乎没有对立。真正张力来自 8087 的“技术超前却生态失败”:它早在 1980 年就支持 80 位浮点,却从未真正普及,甚至被数值计算领域忽视;而 46 年后的许多微控制器仍只有 32 位浮点或根本没有浮点,反倒集成安全岛和能联系外星人的无线电。

认为 8087 在 1980 年就已具备惊人的 80 位浮点能力,却未能延续到后来的生态中

  • 评论者普遍觉得 8087 很酷、能力超前:它早在 1980 年就支持 80 位浮点;但一方面它从未真正流行,连数值计算人群都忽视它,另一方面 46 年后的许多微控制器仍只有 32 位浮点或完全没有浮点,却集成安全岛和能联系外星人的无线电,这种反差尤其令人不满。(较多人) > 我始终不理解,为什么做数值计算的人会无视 8087(@WalterBright)

称赞 Ken 是低质内容海洋中的灯塔

  • 有评论者感谢 Ken 多年持续发布大量文章与视频,把他视为信息低质化环境中的明灯,代表读者对作者长期高质量科普的敬意。(少数人) > Ken 是低质内容海洋里的灯塔,感谢他多年来无数的文章与视频(@kar1181)

有价值的补充

  • 早期 Win32 在上下文切换时不会保存 8087 寄存器;WalterBright 联系微软后,对方才改为保留这些寄存器。他一直以自家编译器支持 8087 为荣。(@WalterBright)