我最近一直在 16 位 DOS 上编程一些东西来娱乐。我看到很多人提到远指针比近指针慢并且要避免它们。
为什么?
从装配的角度来看,这是有道理的。涉及一些额外的指令。您必须存储段寄存器的旧值,将新段存储到寄存器中,然后将其移到 CS 或 DS 中(立即模式存储在 8086 中不是有效的操作码)。然后您可以在该部分中执行您需要执行的任何操作。之后,您必须恢复旧值。
听起来很多,但实际上这并没有消耗很多周期。我想如果您使用的每个指针都在不同的段中,这可能会加起来,但数据通常是分组的。因此,除非你到处弹跳(由于其他原因而速度很慢),否则处罚应该不会那么严重。如果您必须使用 DRAM,那么这应该会主导成本,对吗?
我觉得这个故事还有更多内容,但我很难找到它。希望 8086 向导能记住这些东西。
澄清一下:我对实际的 16 位处理器感兴趣,例如实模式下的 8086 和 80286。
最佳答案
Why are far pointers slow?
段寄存器加载对于核心前端来说过于复杂,无法转换为微操作。相反,它们是由存储在小型 ROM 中的微操作“模拟”的。这从一些分支开始(它是哪种 CPU 模式?),通常这些分支无法从 CPU 的分支预测中受益,从而导致延迟。
为了避免段寄存器负载(例如,当多次使用相同的远指针时),软件倾向于使用更多的段寄存器(例如倾向于使用 ES
、FS
和GS
);这会为指令添加更多前缀(段覆盖前缀)。这些额外的前缀也会减慢指令解码速度。
I guess if every pointer you were using were in a different segment this could add up, but data is usually grouped.
编译器没那么聪明。如果一小段代码使用 4 个远指针,而这些指针恰好都使用同一段,则编译器不会知道它们都在同一段中,并且无论如何都会执行昂贵的段寄存器加载。要解决这个问题,您可以将数据描述为结构(例如,1 个指向具有 4 个字段的结构的指针,而不是 4 个不同的指针);但这要求程序员以不同的方式编写软件。
举个例子;如果你执行类似“int foo(void) { int a; return bar(&a); }”的操作,那么 ss
可能会在堆栈上传递,然后被调用者 (bar
code>) 会将其加载到另一个段寄存器中,因为 bar()
必须假设指针可以指向任何地方。
另一个问题是有时数据大于段(例如 100000 字节的数组不适合 64 KiB 段);因此,某人(程序员或编译器)必须计算并加载不同的段才能访问(部分)相同的数据。所有指针算术可能都需要考虑到这一点(例如,看起来很简单的 pointer++;
可能会变得更像 offset++; segment += (offset >> 4); offset &= 0x000F;
导致段寄存器加载)。
If you have to hit DRAM, that should dominate the cost, right?
对于实模式;您的 RAM 限制为大约 640 KiB,并且 CPU 中的缓存通常要大得多,因此您可以预期每次内存访问都会命中缓存。在某些情况下(例如,每个核心具有 1 MiB L2 缓存的级联湖 CPU),您甚至不会使用 L3 缓存(全部是 L2 命中)。
您还可以预期段寄存器加载比缓存命中更昂贵。
Hoping an 8086 wizard is hanging around who remembers this stuff.
当人们说“分段很糟糕,应该避免”时,他们想到的并不是已经过时 40 年的 CPU (8086),而是本世纪相关的 CPU。他们中的一些人可能不仅仅考虑性能(特别是对于汇编语言程序员来说,分段是一种烦恼/额外的负担)。
关于performance - 为什么远指针很慢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73146953/