Hacker News 中文摘要

RSS订阅

Intel 8087浮点芯片微码:scale指令 -- Microcode in Intel's 8087 floating-point chip: the scale instruction

1970年代,浮点运算混乱,制造商有十几种不兼容算术标准,浮点系统侧重硬件简单而非数学严谨导致数值稳定问题。1980年英特尔(Intel)推出8087浮点协处理器(8087 floating-point coprocessor),力求准确。它可装入国际商业机器(IBM) PC,使浮点运算快达100倍,并成为当今多数计算机浮点标准。8087以微码实现指令,某逆向小组近期进展,正检查其FSCALE(Floating-point Scale)指令微码,该指令按2的幂缩放数快于乘法,原以为极简单。

正文

20 世纪 70 年代,浮点运算一片混乱。计算机制造商有十几种互不兼容的算术标准,而且浮点系统围绕硬件简单性设计,而非数学严谨性,带来数值稳定性问题。1980 年英特尔推出 8087 浮点协处理器芯片,力求尽可能精确,即便在极端情况下也是如此,局面由此改变。8087 可以装进 IBM PC,使电子表格、CAD 等应用的浮点运算速度提高最多 100 倍,因而大受欢迎。更重要的是,8087 成为当今大多数计算机采用的浮点标准。

8087 用称为微程序的复杂底层代码实现指令。我所在的 Opcode Collective 小组正在逆向工程这些微程序,最近取得了一些进展。这次我分析 8087 的 FSCALE 指令的微程序,说明它如何工作。FSCALE(Floating-point Scale)提供把数值乘以 2 的幂的快捷方法,比乘法快得多。我本以为 FSCALE 简单到几乎不值一提,应该很容易理解和讲解。剧透一下:它一点也不简单。FSCALE 用了 140 多条微指令和三层子程序调用,处理许多特殊情况。但 FSCALE 微程序展示了 8087 许多有趣的部分,比如移位器、加法器和指数转换器,还揭示出一个隐藏功能。

为了研究微程序,我打开一颗 8087 芯片,用显微镜拍了高分辨率图像。巨大的微程序 ROM 位于中央,存放控制芯片的 1648 条微指令。左侧的微程序引擎逐步执行微程序,处理跳转和子程序调用。芯片下半部是“数据通路”,即执行浮点计算的电路,分为处理指数的 16 位数据通路和处理有效数字(也叫小数部分)的 64 位数据通路。

Intel 8087 浮点单元芯片裸片,标出主要功能模块。裸片尺寸 5mm×6mm。点击查看大图。

放大芯片底部可以看到数据通路电路,下图标出了与 FSCALE 相关的部分。指数 ROM 存放各种常量。指数转换器是专门电路,检查指数、检测特殊值并在不同指数格式间转换。移位器是个大部件,可以把 64 位数值左右移动任意位数。加法器是 8087 计算的核心;乘、除、开方都在循环中使用它。B 寄存器保存加法器的一个输入,另一个输入可以有多个来源。和寄存器保存加法器输出。八个栈寄存器和临时寄存器存放浮点数。

8087 数据通路特写,标出 <code>md5-b65d3bf91b2e9249ac7c00c4b048fab3</code> 用到的功能模块。

8087 的细节

要使用 8087,程序员把数值存入八个内部寄存器,它们组织成栈。每个寄存器保存一个 80 位浮点数。为提高性能,寄存器栈中每个值都带一个“标签”,对程序员基本不可见。标签把值标记为 valid(有效)、special(特殊)、zero(零)或 empty(空)。正常的浮点值标记为 valid。如果值是无穷大、NaN(Not a Number)或非正规化值,就标记为 special。零值标记为 zero。寄存器为空(比如值已弹出栈)就标记为 empty。

8087 还有内部使用的临时寄存器:tmpA、tmpB 和 tmpC。与栈寄存器一样,tmpA 和 tmpB 是 80 位寄存器,另加两个标签位。tmpC 则只保存 64 位有效数字。

8087 支持多种数据类型:各种大小的浮点数、整数和二进制编码十进制。但内部一律以 80 位浮点数“临时实数”存储;下面只考虑临时实数值。一个数由三部分组成:符号位、15 位指数和 64 位有效数字。多数情况下,浮点数表示为 sign × significand × 2^exponent。有效数字是形如 1.bbb... 的 64 位二进制数:前导 1,然后是二进制小数点(相当于十进制小数点)和其余位。浮点数之所以有用,是因为借助指数(范围 -16382 到 16383),它的覆盖范围从极小到极大。一个重要细节是,指数存储时加上了 16383 的“偏置”,因此存储的指数始终为正,即便实际指数为负。

80 位临时实数格式。三角形表示二进制小数点,相当于十进制小数点。出自 Intel Numerics Supplement。

8087 支持几种用特殊指数表示的特殊值,如下图所示。零和无穷大都有正负之分。NaN 代表没有意义的值,比如 0/0 或 sqrt(-1);NaN 有很多种表示,不只是一个值。8087 还支持非正规化值和未正规化值,它们是非常小的值,有效数字没有前导 1。

特殊值的编码。基于 Intel Numerics Supplement 表 S-31,但做了高度简化。“x”位可任意,只要不与另一类型冲突。

8087 有复杂的异常系统,六种异常用来表示算术运算是否出了问题。最严重的是“非法操作”,表示运算没有意义,比如 0/0 或 ∞-∞,也包括访问空寄存器(栈上溢或下溢)或对 NaN 值做运算。还有结果太大而无法存储的上溢异常、结果太小的下溢异常、除零异常(0/0 除外)。非正规化操作数异常表示结果太小,无法作为正常值存储,但可以存为非正规化值。最后是精度异常,表示一个值无法精确表示、必须舍入(精度异常非常常见,连 1/10 都会触发)。

8087 对每种异常都提供精细控制,由控制寄存器中的位指定。如果异常未屏蔽,8087 会向 8086 处理器发中断,由软件处理,比如终止程序或记录错误。如果异常被屏蔽,8087 会尽力继续执行:非法结果用 NaN 取代,上溢或除零用无穷大取代,精度异常则进行舍入。屏蔽异常的用意是让计算继续,得出尽可能准确的结果;多数情况下这正是程序员想要的。

这些特性让 8087 灵活而精确,也让微程序复杂得多,因为各种特殊情况的组合都要妥善处理。

执行一条 8087 指令可能需要数百个内部步骤,这些步骤由微指令实现的微程序完成。微程序 ROM 存放实现 8087 指令集的 1648 条微指令。我正与 Opcode Collective 合作逆向微指令,弄清全部微程序。

8087 微指令很复杂,有很多边角情况和临时功能,这里只给个简化概览。每条微指令 16 位,如下图所示。前三位指定微指令类型,决定其余位的含义。第一种是传送操作,把数据从一个内部寄存器移到另一个,两个字段分别指定源和目的,剩下三位用于各种特殊情况。第二种是移位操作,用桶形移位器把值左移或右移。第三种控制加法器(也可做减法)。杂项指令包括栈指针操作、标签修改、异常和子程序返回。远跳转和远调用微指令跳转或调用固定列表中的目标微地址。条件字段允许依据多种条件进行条件跳转/调用/返回,最后一位对条件取反。局部跳转是跳到附近微指令的相对跳转。

8087 微指令的结构。

FSCALE 微程序

8087 开始执行指令时,指令译码电路确定该指令微程序的起始地址。这个 11 位地址装入微程序引擎,开始执行微程序。FSCALE 的微程序从十进制地址 748 开始。

FSCALE 的思路很直接:要把浮点数乘以 2^N(N 为整数),只需给数的指数加上 N,比完整浮点乘法快得多。然而 FSCALE 的微程序出人意料地复杂,用了好几个微程序子程序。简言之,它先检查参数是否为零,再处理其他特殊参数;把缩放参数转成整数并与指数相加;最后处理上溢或下溢。

说得更细一点:例程先把栈顶(st(0))的第一个参数移到临时寄存器 tmpA。如果参数是零,例程立即返回(因此用任何东西缩放 0——哪怕 NaN——结果都是 0)。接着把栈上第二个值(第二个参数)移到 tmpB。同样,如果这个值为 0,代码返回,于是用 0 缩放任何值都保持原值不变。然后选择一个常量;选择常量和用它是两条独立微指令(8087 分别有存 16 位指数常量和 67 位有效数字常量的 ROM,这里是指数常量)。正常情况下,执行跳到地址 #0763,跳过对子程序 SPECIAL_TMPS 的调用。

``` FSCALE:

0748 st(0) -> tmpA 从栈顶取输入参数

0749 若 tmpA:tag ZERO 则跳 #0776 为 0 则退出

0750 stackPtr++

0751 st(0) -> tmpB 从 stack(1) 取缩放参数

0752 stackPtr--

0753 若 tmpB:tag ZERO 则跳 #0776 为 0 则退出

0754 expconst 0x403e 常量 403e:转换为整数所需的指数移位

0755 若 tmp 非 empty/special/div 则跳 #0763

0756 call SPECIAL_TMPS 特殊处理

0757 若 flag 则跳 #0762

0758 若 tmpB:tag 非 SPECIAL 则跳 #0761

0759 except:invalid 非法异常,用 NaN

0760 NaN -> tmpA

0761 若 intr 则跳 #0776

0762 若 expConv[0] 则跳 #0775 若 expConv 置位则返回 tmpA,否则继续

0763 tmpB:exp -> Breg 常规路径

0764 tmpB:sign,exp -> expConv ExpConv 将检测 tmpB 的符号

0765 expConst -> tmpC 常量 403e

0766 adder: tmpC - Breg cin=1 403e-exp 即把 tmpB 转成整数所需的移位量

0767 sumreg:frac -> shiftcount 存入移位器控制

0768 shift tmpB:frac R count byte bit 执行移位

0769 shift R -> Breg Breg 保存整数形式的缩放参数

0770 若 neg 则跳 #0777 负的 Breg 需单独处理

0771 adder: tmpA:exp + Breg cin=0 把缩放量加到指数

0772 sumreg:frac -> expConv 结果送入 expConv 检查

0773 sumreg:frac -> tmpA:exp 用和更新指数

0774 若 exp 非 normal 则 call NONNORMAL_RESULT 处理上溢/下溢

0775 tmpA -> st(0) 结果写回栈

0776 RNI 完成:运行下一条指令

0777 adder: tmpA:exp - Breg cin=1 减去 Breg

0778 jmp #0772 继续处理

```

在 #0763,第二个参数从浮点数转成整数,需要几步。例如参数是 9,浮点表示为 1.001×2^3。有效数字位 1000 是“左对齐”的,但作为整数需要用右移把它们“右对齐”。一般来说,如果指数是 n,有效数字要右移 63-n 位。但指数偏置了 16383,所以有效数字必须右移 63-(exp-16383) 位,即 0x403e-exp 位。(这解释了微程序前面出现的常量 0x403e。)

通过移位把浮点数转换为整数。

微程序中,减法要几步完成。在 #0763,第二个参数的指数移入 B 寄存器——加法器的一个输入(与 tmpB 完全不同)。接着,符号和指数移入指数转换器,这个电路会检测溢出等。然后,常量 0x403e(在 #0754 选好的)移入 tmpC 寄存器。在 #0766,加法器被激活,从常量里减去指数。加法器把结果放进和寄存器,这个值再复制到移位计数寄存器,控制移位器。它表示第二个参数需要移多少位才能转成整数。在 #0768,移位器按所需位数移位,同时使用位移动和字节移动部分。与加法器一样,激活移位器和读取结果是两条独立微指令;结果放入 B 寄存器。

FSCALE 的核心部分最终在 #0771 执行,把第二个参数加到第一个参数的指数上。加法器被激活,把 B 寄存器值(缩放量)加到指数上,更新后的值存入 tmpA 的指数。(缩放因子为负时则走 #0777 路径做减法。)这个值同时送入指数转换器,检查指数是否上溢或下溢;如果溢出,就调用子程序 NONNORMAL_RESULT。正常情况下,更新后的值从 tmpA 复制到栈顶寄存器 st(0)。最后,RNI(Run Next Instruction)表示微程序例程结束,指令完成。因此,即便在最直接的情况下,FSCALE 也要用大约 22 条微指令。

Image 7: The Intel 8087 chip is packaged in a 40-pin DIP (dual in-line package), as are the 8080 and Z80. This photo is here as a break from all the microcode.

处理空或特殊参数

如果参数访问空的栈位置(即栈下溢)或是特殊值(无穷大、非正规数、NaN)会怎样?这些情况由一个微子程序处理,我叫它 SPECIAL_TMPS,因为它处理 tmpA 和/或 tmpB 中的特殊值。这是个通用例程,基本算术运算、FSCALE、FTST(测试)和 FPREM(部分余数)都会用。

SPECIAL_TMPS 的控制流相当曲折,因为如果一个参数为空而另一个是非正规数,代码必须排定优先级。这里只做简短总结。子程序先把任何非正规数转成未正规数,然后检查是否访问空栈位置,若是则触发异常或中断。接着再检查两个参数:如果任一是 NaN,就触发异常或中断;否则返回一个状态,说明参数类型。

出乎意料的是,如果两个参数都是 NaN,代码会比较两个 NaN 值并返回较大的那个。这种行为看起来很怪,但这是有文档记载的特性。你可能以为 NaN 是一个值,其实它是个庞大的家族。当初的想法是,程序员可以用不同的 NaN 值标记问题出现的位置。比如在未初始化数组的每个位置放不同的 NaN,就能知道访问了哪个位置。出于某种原因,8087 设计者决定:两个不同 NaN 做运算时,结果取较大的那个。因此微程序需要代码检测两个操作数是否都是 NaN 并计算较大者,用减法做比较(#1518)。

``` SPECIAL_TMPS (J5):

1484 若 tmpA:tag SPECIAL 则 call SPECIAL_VAL 处理 tmpA/tmpB 中的特殊值

1485 xchg tmp

1486 若 tmpA:tag SPECIAL 则 call SPECIAL_VAL 处理 tmpB 特殊值

1487 xchg tmp

1488 1 -> flag 默认 flag=1

1489 若 tmp 非 empty/special/div 则跳 #1500 tmp 正常则 0 -> expConv

1490 1 -> expConv

1491 若 tmpA/B 非 empty 则跳 #1497

1492 except:invalid 任一为空则非法

1493 若为比较指令则跳 #1525 比较指令不产生 NaN

1494 若 intr 则跳 #1511 中断未屏蔽则返回

1495 NaN -> tmpA 中断被屏蔽则用 NaN

1496 return

1497 若 tmpA:tag SPECIAL 则跳 #1502 特殊情况

1498 若 tmpB:tag SPECIAL 则跳 #1505

1499 0 -> flag 除法常规路径:

1500 zero -> expConv 返回 flag 0, expConv 0

1501 return

1502 call SPECIAL_VAL TmpA 特殊

1503 若非 flag 则跳 #1512 若为 NaN 则跳转,否则继续(无穷大)

1504 若 tmpB:tag 非 SPECIAL 则跳 #1509

1505 xchg tmp TmpB 特殊

1506 call SPECIAL_VAL

1507 xchg tmp

1508 若非 flag 则跳 #1521 若为 NaN 则跳转,无穷大则返回

1509 0 -> flag 清 flag,返回

1510 return

1511 RNI 带中断结束指令

1512 若 tmpB:tag 非 SPECIAL 则跳 #1522 TmpA 是 NaN,检查 tmpB

1513 xchg tmp

1514 call SPECIAL_VAL 检查 tmpB

1515 xchg tmp

1516 若 flag 则跳 #1522 tmpB 非 NaN 则跳转

1517 except:invalid 非法异常

1518 tmpB:frac -> Breg 两个参数都是 NaN,取较大者

1519 adder: tmpA:frac - Breg cin=1

1520 若加法器符号则跳 #1522 看 tmpA < tmpB

1521 tmpB -> tmpA 取较大者

1522 except:invalid 非法异常

1523 若为比较指令则跳 #1525 比较指令不产生中断

1524 若 intr 则跳 #1511 带中断结束指令

1525 1 -> flag 带 flag 返回

1526 return J5 结束

```

这个子程序大量使用辅助子程序 SPECIAL_VAL,它处理单个参数:把非正规参数转成未正规参数,视情况触发异常或中断,并标记输入是否为无穷大。

#1485 处交换 tmpA 和 tmpB 的微指令,其硬件很有意思。它并不真的在两个寄存器间搬移值,而是翻转一个触发器,交换 tmpA 和 tmpB 的含义:触发器置位时,对 tmpA 的引用就指向 tmpB,反之亦然。(这是微处理器的标准技巧;英特尔 8080 的 XCHG 指令用类似方式交换 DE 和 HL 寄存器,Z80 也在 EX 和 EXX 指令中用同样手法交换主寄存器组和辅助寄存器组。)

处理非正规结果

拿一个很大的数去放大,可能上溢;拿一个很小的数去缩小,可能得到非正规化数或下溢。这会触发上溢、非正规或下溢异常,未屏蔽时还会中断。而且 8087 支持四种舍入模式:舍入到最近的有效值、向下舍入(朝 -∞)、向上舍入(朝 +∞)、朝零舍掉。根据舍入模式,上溢可能得到 ∞,也可能得到最大可表示浮点数;下溢可能得到零,也可能得到最小可表示浮点数。无穷大还分仿射和投影模式,可以是带符号或无符号。因此 FSCALE 微程序要处理结果的许多特殊情况。

下面是处理 tmpA 中非正规结果的子程序。一条有趣的微指令是 update overflow/underflow exceptions,它会在适当时触发异常。大多数异常由微指令直接触发(比如 #0346 的 except:precision)。但上溢和下溢异常,微程序把任务交给硬件:8087 的“指数转换器”电路根据选定的浮点精度检查指数,判断是否存在上溢或下溢。微指令则根据这些值设置上溢和下溢标志。一个复杂任务就这样靠指数转换器的硬件支持,由一条微指令完成。

``` NONNORMAL_RESULT (J16):

0318 若 tmpA:tag ZERO 则 return 处理非正规结果

0319 update overflow/underflow exceptions 若指数转换器指示则触发异常

0320 expconst 0x6000 中断偏置常量 0x6000

0321 若非 intr 则跳 #0329

0322 expConst -> Breg 中断路径

0323 若 neg 则跳 #0326

0324 adder: tmpA:exp + Breg cin=0 下溢加上偏置

0325 jmp #0327

0326 adder: tmpA:exp - Breg cin=1 上溢减去偏置

0327 sumreg:frac -> tmpA:exp 新指数写入 tmpA

0328 return 有中断,结束

0329 若 neg 则跳 #0344 屏蔽异常

0330 tmpA:exp -> Breg 下溢

0331 adder: 1 - Breg cin=1 非正规数需移位数

0332 call CREATE_DENORM 创建非正规数

0333 adder: zero + Breg cin=0, roundmode 加零以舍入

0334 call ADJUST_PRECISION 按指定精度调整

0335 若和寄存器为零则跳 #0340 为零则视情况返回 +/- 零

0336 zero -> tmpA:exp 非正规数:指数为 0

0337 sumreg:frac -> tmpA:frac 保存非正规数的小数

0338 special -> tmpA tag 非正规数标记为 special

0339 return

0340 tmpA sign -> sign latch 返回 +/- 零

0341 zero -> tmpA

0342 sign latch -> tmpA sign

0343 return

0344 NaN/Inf -> tmpA:exp 上溢:可能返回无穷大

0345 tmpA:frac -> tmpB:frac 把 tmpA 小数存入 tmpB

0346 except:precision 设置精度异常

0347 Inf -> tmpA:frac 小数置为无穷大

0348 special -> tmpA tag 无穷大标记为 special

0349 若非朝零舍入则 return 向上舍入则返回无穷大

0350 1 -> Breg 返回最大浮点数:向下调整

0351 adder: tmpA:exp - Breg cin=1

0352 sumreg:frac -> tmpA:exp 指数=7fff-1=7ffe

0353 adder: zero - Breg cin=1

0354 sumreg:frac -> tmpA:frac 小数 0-1 = ff...ff

0355 norm -> tmpA tag 正常值

0356 若 tmpB:frac[63] 则 return 除非是未正规数,返回最大浮点数

0357 tmpB:frac -> tmpA:frac 返回原 tmpA 小数

0358 return

```

如果上溢或下溢未屏蔽并产生中断,8087 的行为很有意思。它的用意是让中断处理程序知道指数本来应该是多少。但正确值太大或太小,装不进指数字段(这正是异常发生的原因),于是解决办法是加上或减去常量 0x6000,得到一个能装下的指数。中断处理程序再减掉或加上这个常量,就能得到正确指数。#0322 到 #0328 执行这个加或减。

对于已屏蔽的下溢,子程序 CREATE_DENORM 会生成一个非正规值,再由 ADJUST_PRECISION 按指定精度舍入。如果值小到连非正规数都装不下,就视情况返回 +0 或 -0。

对于已屏蔽的上溢,8087 根据指定舍入模式返回无穷大或最大可表示浮点数。无穷大的指数全为 1、有效数字是 1000...,这些值由晶体管直接送上总线。最大浮点数则是算出来的:从无穷大的指数减 1,从零有效数字减 1。

辅助子程序:创建非正规数

8087 有一个颇具争议的特性:非正规数,即比“常规”浮点数更小的数。浮点数的有效数字首位是 1,但如果已经用了最小指数还要更小的数怎么办?8087 允许打破有效数字以 1 开头的规则,产生更小的非正规化数。非正规数大幅扩展了范围,最小可以再小 2^63 倍。但非正规数精度较低,因为高位被“浪费”了;而且与非正规数有关的计算往往明显更慢,因为需要特殊处理。

正常数除以 8 得到非正规数的例子。

上图是一个指数已最小的正常数(-16382,偏置后为 1)。把它除以 8(即缩放 -3)会产生非正规数,因为指数不能再减小了。于是有效数字右移 3 位,指数换成特殊值 0,表示这是非正规数。

在 8087 中,非正规数由一个微程序子程序创建,我叫它 CREATE_DENORM,许多算术运算都用它,不止 FSCALE。该子程序接收一个正常数和一个移位量,通过移位(如上例)生成非正规化数。下面的微程序用指数转换器检查移位量是否达到或超过 64;若是,移位后什么都不剩,返回零。否则把值右移,非正规数存入 B 寄存器。

``` CREATE_DENORM (J20):

0522 sumreg:frac -> expConv 创建非正规数

0523 sumreg:frac -> shiftcount 移位数

0524 若 exponent[6:14] == 0 则跳 #0528 小于 64 则跳转

0525 zero -> Breg 没有位留下,用零

0526 shift tmpA:frac L 0 bytes, 0 bits 穿过移位器?

0527 jmp #0532

0528 shift tmpA:frac R count byte bit 按要求右移

0529 shift R -> Breg 结果送入 Breg

0530 shift tmpA:frac L ~count byte bit 再移回来做粘滞位检测

0531 NOP 等待移位器

0532 rounding(h) -> Breg[grs] 把三个舍入位存入 Breg

0533 return

```

Image 11: The architecture of the 8087. Based on the patent. Click this image (or any other) to magnify.

但为什么之后又把值左移(#0530)?目的是取得舍入位。8087 的一条原则是舍入必须正确,这比看起来难得多。要决定如何向上舍入,需要跟踪多得不可能的位。比如计算 1+0 再向上舍入,得 1;但计算 1+2^-10000 再向上舍入,会得到比 1 略大的浮点数。问题是在舍入前如何区分两者,又不存几千位。

诀窍是 8087 保留三个用于舍入的位:“保护”位、“舍入”位和“粘滞”位。把有效数字右边看作一条“尾巴”,保护位是尾巴的最高位,接着是舍入位。粘滞位很特别:它是尾巴所有剩余位的或,表示其中是否有任何一位为 1。于是 1+2^-10000 的粘滞位被置位,而 1+0 没有,两者就能以不同方式向上舍入。为生成粘滞位,8087 用一个非常大的 64 位或非门并行检测尾巴各位。

右移时如何计算保护位、舍入位和粘滞位的示意图。图中数字与上一例不同。

当一个数右移(比如创建非正规数)时,右边的位会丢失。为生成舍入位,程序把值向“左”移,保留最终会被丢弃的尾巴位,同时丢弃最终有效数字中的位。最高两位变成保护位和舍入位,其余位相或生成粘滞位。上图就是一例:假设值要右移 4 位,尾巴位 abcd(至少 d)会在移位中丢失。把原有效数字右移 59 位(4 的补数)来计算舍入位:第 62 位 a 成为新保护位,第 61 位 b 成为新舍入位,剩余 64 位相或成为新粘滞位。(旧有的保护、舍入、粘滞位也会或进来,不会丢失。)把第一次移位的有效数字与第二次移位得到的舍入位合并,就得到所需结果。

辅助子程序:调整精度

虽然 8087 支持三种长度的浮点数,但所有计算都用 80 位“临时实数”进行。指令结束时,结果才转成所需长度(因此用较短浮点数,大多数指令并不会更快)。有个我称为 ADJUST_PRECISION 的微程序子程序,按控制字指定的精度、用指定舍入模式转换结果。大多数算术指令都用它。

8087 支持三种实数。出自 Intel Numerics Supplement。

第一条代码路径处理临时实数(64 位精度)。控制字指定四种舍入模式之一。但对特定有效数字,实际只有两种动作:向下舍入(截断)或向上舍入(截断后加 1)。这个决定由复杂逻辑电路做出,电路检查舍入位、舍入模式和符号,确定该向上还是向下。这简化了微程序,却让硬件更复杂。微程序执行条件返回:若有效数字不需要向上舍入就直接返回;否则给有效数字加 0 并置进位输入,使其加 1。然后检查是否溢出,若溢出就用无穷大替换,并设置特殊标志。

Image 12: A flowchart for the SPECIAL_TMPS subroutine. Click for a larger version.

``` ADJUST_PRECISION (J11):

0299 若非 precision64 则跳 #0306

0300 若非向上舍入则 return,并更新 CC1 更新条件码,可能返回

0301 adder: sumreg:frac + 0 cin=1 加 1 以向上舍入

0302 若非 sumreg[64] 则 return

0303 Inf -> sumreg:frac,sign 溢出则返回无穷大

0304 2count++ 设置特殊标志

0305 return

0306 23/52 -> shiftcount 短或长实数:取相应移位量

0307 shift sumreg:frac,rnd L count byte bit sticky 移位生成舍入位

0308 NOP 等待移位器完成

0309 rounding(H) -> sumreg[grs] 存入舍入位

0310 shift sumreg:frac R ~count byte bit 右移丢弃多余位

0311 shift R -> sumreg:frac

0312 若非向上舍入则跳 #0314,并更新 CC1 更新条件码

0313 adder: sumreg:frac + 0 cin=1 视情况向上舍入

0314 shift sumreg:frac L ~count byte bit 左移回正

0315 shift L -> sumreg:frac,sign

0316 若非 sumreg[64] 则 return 未溢出则返回

0317 jmp #0303 返回无穷大

```

返回较小精度(短实数或长实数)时,代码更复杂,因为有效数字必须缩短。首先 #0306 按控制字指定的精度把 23 或 52 装入移位器,然后把值左移,像上一节那样产生舍入位。接着把值右移,缩短到目标长度。和之前一样,视是否需要向上舍入来给有效数字加 1 或不变。最后把值左移回去,使有效数字最高位在左边。同样,如果向上舍入导致溢出,就返回无穷大。

一个奇怪之处是,带“向上舍入”条件的跳转还有一个副作用:更新 8087 程序员可见的条件码寄存器 CC1,表示结果被向上还是向下舍入。也就是说,8087 有额外电路检测这一特定条件并把值装入条件码锁存器。奇怪的是,8087 文档没有描述这个条件码动作;英特尔直到 1987 年的 387SX 浮点芯片才记录下来。

结论

浮点技术的历史远比 8087 悠久。例如 IBM System/360 大型机(1964 年)就支持 32 位和 64 位浮点数。1977 年 AMD 推出 Am9511 浮点芯片,支持 16 位和 32 位浮点数以及超越函数。8087 的革命性在于它精心设计、力求数学上尽可能精确,这主要归功于数值专家 William Kahan。(8087 促成了 IEEE 754 标准,如今几乎所有计算机都在用,终结了互不兼容浮点标准的无政府状态。)

8087 最终变得极其复杂:三种浮点数长度、四种整数长度、四种舍入模式、无穷大模式、一堆可屏蔽或不可屏蔽的异常、非正规化和未正规化数、带符号和无符号无穷大、带符号零,还有整整一个 NaN 家族。这些特性组合起来产生许多边角情况。8087 既靠专门电路,也靠纷繁复杂的微程序来应付这种复杂性。

8087 有多复杂?对于没有 8087 芯片的用户,英特尔出售过 8087 Support Library,能精确模拟 8087 的指令(但慢得多)。这个模拟器占 16K 字节的 8086 代码,而在当时一个完整的 BASIC 解释器才 8K。换个角度看,8087 的硬件大幅减少了所需软件:8087 本身只用 3.3K 微程序,而模拟器用 8086 代码要 16K。

评论

讨论围绕 8087 协处理器展开,不少人质疑 x87 架构怪异、对编译器极不友好,也有人盛赞其带来的巨幅提速与非对称并行设计巧思。

认为 x87 架构怪异且对编译器极不友好

  • 指出其设计思路类似科学计算器,与现代 CPU 工作逻辑脱节,导致编译适配痛苦,软硬件层面都倾向规避并改用 SIMD;80位寄存器宽度也显得来源不明(较多人) > 这架构简直像为惠普逆波兰计算器设计的,让编译器生成代码苦不堪言,大家能躲就躲改用 SIMD 了(@cogman10)

认为 8087 的性能提升与并行设计极具价值

  • 亲历者表示百倍数学提速绝非夸张,且在旧机器上体验过 3 秒对 300 秒的悬殊差距;其指令流可与 x86 交错形成非对称多处理器,让浮点与整数代码真正并行(较多人) > 8087 指令能和 x86 交织在一起,等于构成了双芯片并行工作的异步多处理系统(@garganzol)

有价值的补充

  • 从单一缩放指令扩展到 140 条微指令的实现方式,使特殊情形处理机制变得更加直观具体(@finn888)
  • 本文作者现身帖子,表示可解答关于 8087 的疑问(@kens)