微软旧编译器8087浮点模拟代码的历史可追溯至1980年代初
来源标题:Code Smells of Desperation (2023)
微软C编译器的8087浮点模拟库代码可能源于更早的MS Pascal 3.0,后者在1982年底或1983年初已包含该库。QuickBASIC 3于1987年加入8087支持,QuickBASIC 4默认同时使用8087代码和模拟器,而BASIC 6因模拟器速度较慢又改回类似pre-8087的浮点库。
以下正文由 AI 翻译自 Hacker News(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
这篇内容可能比 Microsoft C 还要早。MS Pascal 3.0 就带有一个仿真库,而它发布于 1983 年初或 1982 年末,远早于 MS C 3.0(第一个在 Microsoft 内部编写的 C 编译器)。我想知道其中一些代码是否可以追溯到 MS BASIC。也许不是,或者说可能并不完全一样,因为最早的 MS 工具使用不同的格式来表示二进制数。MS Pascal 3.0 使用 IEEE 784 格式,并且也支持真实的 8087。据我所知,MS Pascal 2.0 既不支持仿真,也不支持真实的 8087。这并不太令人意外,因为在早期的 PC 时代,8087 尚未面世。而且没错,早期的 MS 语言使用一个“无限”的 FPU 栈,在正常操作期间可能触发异常。我相信到 1980 年代末左右,这种用法已经不再使用了。MSBASIC 在 1987 年通过 QuickBASIC 3 加入了对 8087 的支持,而 QuickBASIC 4 默认使用 8087 代码以及一个仿真器。所以 BASIC 是在 C 之后才获得这一支持的,而且很可能使用的是 C 版 8087 仿真器的一个变体。BASIC 6 又改回了包含类似 pre-8087 浮点库的东西,因为那些库比 8087 仿真器更快。有不少第三方库确实为 MS BASIC 提供了与 8087 的接口,但我从未用过,而且快速搜索也找不到相关信息。Startz 的书展示了一些将 ASM 例程与 8087 接口的技术,看起来相当繁琐。这让我庆幸自己在 80 年代大部分使用数值协处理器的代码都坚持用了 FORTRAN。我没有找到 WIN87EM 的代码,但我怀疑必须有某种东西来替代那些需要已安装 8087 才能响应的浮点 WAIT 指令。哈哈,读这样的帖子很有怀旧感。让我想起 1990 年代在台式机上编程的日子。你可能会对我那本小中篇小说《Cheap Complex Devices》感兴趣,里面有一个很长的短篇故事叫《Bees, or The Floating Point Error》。事实上,在对《Cheap Complex Devices》作者身份的几种可能解读之一中,整部中篇小说可能是一个昏迷的计算机设计师大脑的输出,经过一个 somewhat sentient、而且绝对满是 bug 的浮点处理器处理而成。无论如何,你会在里面找到一些只有真正的浮点错误痴迷者才能欣赏的极客笑话。我不会用链接来刷屏,但这本书可以在常见的网站上购买(附带大量好评)。也可以在这里免费下载:https://dl.bookfunnel.com/1e7142jomu
在我看来,这段代码非常、非常努力地试图避免使用 FWAIT。如果 80287 与 80286 之间的连接不可靠(但可靠到足以让普通的 80286 代码正常工作),我就会写出这种代码。80287 芯片是否存在常见的不可靠问题,或者只是没有正确插入插座?那里的注释确实散发着绝望的气息(你可以通过搜索“emoemwin.asm”找到这个处理程序的源代码,然后查看 __fpIRQ13),但其中有一些有趣的片段,比如“486 bug——必须在最后一次‘out f0’之后等待,以清除 FP 异常,否则 IGNNE# 将永久处于激活状态。”鉴于 Microsoft 处于早期步进版 386 和 486 异常行为的前沿,这可能不仅仅是无稽之谈。我认为,尽管最终的代码看起来非常像胡言乱语,但它很可能确实解决了某个人们理解不清的问题。而且没错,它很可能是在本身就有 bug 的预发布硬件上出现的问题。带有内置 FPU 的 486 与更老的 CPU/FPU 相比,需要不同的主板支持电路。几乎可以肯定 Microsoft 拥有预发布的 486 系统,而且这类系统很可能存在“有趣的”bug。Intel 在 486 数据手册的早期修订版中修改了 FERR# 和 IGNNE# 的文档,这表明文档即使不算错误,也至少是被人理解得很差。这段源代码还说明了此类代码的一个主要问题:它没有解释自己究竟在试图解决什么问题。这意味着下一个程序员无法轻易丢弃这种看起来荒谬的代码,因为他们无法确定它是否在解决某个相对普遍且合理的问题。测试也不会有太大帮助,因为不能假定相关硬件实际上可用于测试。
当我在复古的东西中看到那些看起来像是疯狂之举的代码时,我反而会欣赏它们,将其视为某种扭曲的才华。如果在 CPUID 时代之前,同一段代码需要在跨越多个 CPU + FPU 世代、带有各种已知勘误(甚至还有更多 NDA 保密勘误,也许甚至是在官方记录为勘误之前你自己发现的 bug)的系统上运行,我会认为能在每个系统上运行的最低共同标准必然是一个大杂烩。在 I/O 端口写入之间插入的整个 POP/PUSH 循环看起来像是某种 NOP/等待状态,我记得在某些高速系统中,为了绕过慢速 I/O 访问,这是必要的,就像这样:https://forum.vcfed.org/index.php?threads/io-delays-on-8088-class-computers.45713/ 我想,如果能看看开发者的笔记,了解那段代码是如何随着测试系统不断失败而演变的,直到他们最终确定下来,那一定会很有趣。这不是才华,这是绝望。我自己也经历过;当硬件不按预期工作时,你只能尝试不同的东西,直到某个方法(希望)奏效。但你可能永远不知道它为什么有效,也肯定不知道它是不是最佳的变通方案。是的,PUSH/POP 循环是为了放慢速度,比单纯的“空”LOOP 指令更慢。问题在于,这些循环的速度在不同 CPU 型号之间变化极大。因此,它们在较慢的 CPU 上浪费了大量时间,而在较新的 CPU 上可能仍然运行得太快。
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。