为什么 x86很丑? 为什么它被认为比别人差?

我已经阅读了一些 SO 文档,并遇到了针对 x86体系结构的语句。

还有更多的评论,比如

我试图寻找,但没有找到任何理由。我并不觉得 x86不好,可能是因为这是我所熟悉的唯一架构。

有没有人能给我一些理由,说明为什么我认为 x86比别人更丑/更差/更差。

38199 次浏览

有几个可能的原因:

  1. X86是一个相对较老的 ISA(毕竟它的祖先是8086s)
  2. X86已经发展了很多次,但是需要硬件来保持与旧二进制文件的向后兼容性。例如,现代的 x86硬件仍然支持本机运行16位代码。此外,还存在几种内存寻址模型,允许较旧的代码在同一处理器上进行互操作,例如 real 模式、 protected 模式、虚拟86模式和(amd64) long 模式。这可能会让一些人感到困惑。
  3. X86是一台 CISC 机器。在很长一段时间里,这意味着它比 MIPS 或 ARM 这样的 RISC 机器慢,因为指令使用的是 数据相互依赖性和标志,这使得大多数形式的指令层级平行很难实现。现代的实现将 x86指令翻译成类似 RISC 的指令,称为“ micro-ops”,以便在硬件中实现这些优化。
  4. In some respects, the x86 isn't inferior, it's just different. For example, input/output is handled as memory mapping on the vast majority of architectures, but not on the x86. (NB: Modern x86 machines typically have some form of DMA support, and communicate with other hardware through memory mapping; but the ISA still has I/O instructions like IN and OUT)
  5. X86ISA只有很少的体系结构寄存器,这可以迫使程序比其他方式更频繁地在内存中来回运行。这样做所需的额外指令需要执行资源,这些资源可以用于有用的工作,尽管 有效的存储转发保持较低的延迟。现代的寄存器重命名在一个大型物理寄存器文件上的实现可以让许多指令继续运行,但是缺乏体系结构寄存器仍然是32位 x86的一个重大缺陷。X86-64从8个整数和向量寄存器增加到16个整数和向量寄存器是64位代码比32位快的最大因素之一(还有更有效的寄存器调用 ABI) ,而不是每个寄存器增加的宽度。进一步增加从16到32个整数寄存器将有所帮助,但不是很多。(AVX512确实增加到了32个向量寄存器,因为浮点代码具有更高的延迟,并且通常需要更多的常量。)(请看评论)
  6. X86汇编代码很复杂,因为 x86是一个具有许多特性的复杂体系结构。一个典型的 MIPS 机器的指令清单可以放在一张字母大小的纸上。X86的等效清单占用了几个页面,而且说明只是做了更多的事情,因此您通常需要比清单能够提供的更详细的说明来说明它们的作用。例如,MOVSB指令需要一个相对较大的 C 代码块来描述它的功能:

    if (DF==0)
    *(byte*)DI++ = *(byte*)SI++;
    else
    *(byte*)DI-- = *(byte*)SI--;
    

    这是一个执行加载、存储和两个加法或减法(由标志输入控制)的单个指令,每个指令在 RISC 机器上都是独立的指令。

    虽然 MIPS (和类似的架构)的简单性并不一定使它们更优秀,但是对于汇编语言类的入门教学来说,从一个更简单的 ISA开始是有意义的。一些汇编类教授 x86的一个称为 Y86的超级简化的子集,这种简化超出了对实际使用没有用处的程度(例如没有 shift 指令) ,或者一些只教授基本的 x86指令。

  7. X86使用可变长度的操作码,这增加了指令解析的硬件复杂性。在现代,随着 CPU 越来越受到内存带宽的限制而不是原始计算的限制,这种成本正在变得越来越小,但是许多“ x86攻击”文章和态度来自这种成本相对较大的时代。
    2016年更新: Anandtech 发布了 关于 x64和 AArch64下操作码大小的讨论

编辑: 这不应该是一个 砸 X86!聚会。考虑到这个问题的措辞,我别无选择,只能进行一些抨击。但是除了(1)之外,所有这些事情都是有充分理由的。英特尔的设计师并不愚蠢,他们希望通过他们的架构实现一些东西,这些就是他们为了实现这些东西而必须支付的一些税收。

我不是专家,但似乎人们不喜欢它的许多特性可能是它表现良好的原因。几年前,使用寄存器(而不是堆栈)、寄存器帧等被认为是使架构对人类来说更简单的很好的解决方案。然而,现在重要的是缓存性能,x86的可变长度单词允许它在缓存中存储更多的指令。我相信反对者指出的“指令解码器”曾经占据了芯片的一半,现在已经不是那样了。

我认为并行性是当今最重要的因素之一——至少对于运行速度已经快到可用的算法来说是如此。在软件中表达高并行性允许硬件分期(或经常完全隐藏)内存延迟。当然,体系结构的未来可能在类似量子计算的领域更加深远。

我听 nVidia 说,英特尔的一个错误是他们把二进制格式保持在硬件附近。CUDA 的 PTX 可以进行一些快速的寄存器使用计算(图形着色) ,所以 nVidia 可以使用寄存器机器代替堆栈机器,但仍然有一个升级路径,不会破坏所有旧软件。

x86 assembler language isn't so bad. It's when you get to the machine code that it starts to get really ugly. Instruction encodings, addressing modes, etc are much more complicated than the ones for most RISC CPUs. And there's extra fun built in for backward compatibility purposes -- stuff that only kicks in when the processor is in a certain state.

例如,在16位模式中,寻址可能看起来非常奇怪; 有一个 [BX+SI]的寻址模式,但没有一个 [AX+BX]的寻址模式。这样的事情往往会使寄存器的使用复杂化,因为您需要确保您的值在一个寄存器中,您可以根据需要使用这个寄存器。

(幸运的是,32位模式更理智(尽管有时候它本身还是有点怪异——例如分段) ,而且16位 x86代码在引导加载程序和一些嵌入式环境之外已经基本上不相关了。)

还有过去英特尔试图让 x86成为终极处理器时留下的遗产。几个字节长的指令执行的任务实际上已经没有人再执行了,因为它们实在是太慢或太复杂了。对于两个示例,ENTER 和 循环指令——注意 C 堆栈框架代码类似于“ push ebp; mov ebp,esp”,而对于大多数编译器来说不是“ ENTER”。

X86架构可以追溯到8008微处理器和相关产品的设计。这些 CPU 是在内存慢的时候设计的,如果你可以在 CPU 模块上完成,它通常比 很多快。然而,CPU 死亡空间也是昂贵的。这两个原因就是为什么只有少数寄存器具有特殊用途,而复杂的指令集具有各种陷阱和局限性。

同时代的其他处理器(例如6502系列)也有类似的局限性和怪癖。有趣的是,8008系列和6502系列都被设计成嵌入式控制器。即使在那时,嵌入式控制器也被期望用汇编程序来编程,并且在很多方面迎合了汇编程序员而不是编译器编写者的需要。(看看 VAX 芯片,当你迎合编译器编写时会发生什么。)设计人员并没有期望它们成为通用的计算平台; 这就是 POWER 架构的前辈们的目的。当然,家用电脑的革命改变了这一点。

在我看来,对 x86的主要批评是它的 CISC 起源——指令集包含许多隐式的相互依赖性。这些相互依赖性使得在芯片上进行指令重新排序之类的操作变得非常困难,因为这些相互依赖性的工件和语义必须为每条指令保留。

例如,大多数 x86整数加减指令修改标志寄存器。在执行加法或减法之后,下一个操作通常是查看标志寄存器以检查溢出、符号位等。如果在此之后还有另一个 add,则很难判断在第1个 add 的结果为已知之前开始执行第2个 add 是否安全。

在 RISC 体系结构中,add 指令将指定输入操作数和输出寄存器,关于操作的一切都将只使用这些寄存器进行。这使得解耦相邻的添加操作变得更加容易,因为不存在强制所有操作排列并执行单个文件的标志寄存器。

DEC Alpha AXP 芯片,一种 MIPS 风格的 RISC 设计,在可用指令方面非常简陋,但是指令集的设计是为了避免指令间隐式寄存器依赖。没有硬件定义的堆栈寄存器。没有硬件定义的标志寄存器。甚至指令指针也是 OS 定义的——如果你想返回给调用者,你必须弄清楚调用者将如何让你知道返回到哪个地址。这通常由 OS 调用约定定义。但是在 x86上,它是由芯片硬件定义的。

无论如何,在超过3或4代的 Alpha aXP 芯片设计中,硬件从一个有32个 int 寄存器和32个 float 寄存器的斯巴达指令集的字面实现,变成了一个有80个内部寄存器、寄存器重命名、结果转发(前一个指令的结果被转发到依赖于值的后一个指令)和各种疯狂的性能增强器的大规模无序执行引擎。所有这些花里胡哨的东西,使得 AXP 的芯片死亡率仍然比当时的奔腾芯片死亡率要小得多,而且 AXP 的速度要快得多。

在 x86系列中,您不会看到这种性能提升的爆发式增长,主要是因为 x86指令集的复杂性使得许多类型的执行优化即使不是不可能,也是非常昂贵的。英特尔的天才之举就是放弃在硬件上实现 x86指令集——所有现代的 x86芯片实际上都是 RISC 核心,它们在一定程度上解释 x86指令,将它们转换成内部微代码,这保留了原来 x86指令的所有语义,但允许一些 RISC 无序和其他微代码优化。

我已经写了很多 x86汇编程序,可以充分理解其方便的 CISC 根。但是直到我花了一些时间编写 Alpha AXP 汇编程序,我才完全理解 x86有多么复杂。我被 AXP 的简洁和统一惊呆了。两者之间的差异是巨大而深远的。

除了人们已经提到的原因:

  • X86-16有一个相当奇怪的 存储器寻址方案存储器寻址方案,它允许以多达4096种不同的方式寻址单个内存位置,将 RAM 限制为1MB,并迫使程序员处理两种不同大小的指针。幸运的是,向32位的转移使这个特性变得不必要了,但是 x86芯片仍然带有段寄存器的残渣。
  • 虽然这不是 x86 per se的错误,但是 X86调用约定并没有像 MIPS 那样标准化(主要是因为 MS-dos 没有任何编译器) ,留给我们的是一团糟的 __cdecl__stdcall__fastcall等等。

我认为,如果您曾经尝试编写一个以 x86为目标的编译器,或者如果您编写一个 x86机器模拟器,或者甚至如果您尝试在硬件设计中实现 ISA,那么您将得到部分答案。

虽然我知道 X86很丑参数,我仍然认为编写 x86汇编比编写 MIPS (例如)更多的是 有趣,后者只是单调乏味。它总是对编译器友好,而不是对人类友好。我不确定芯片是否会对编译器编写者更有敌意,如果它尝试..。

对我来说,最丑陋的部分是(实模式)分段的工作方式——任何物理地址都有4096段: 偏移量别名。你上次这样做是什么时候?如果段部分是32位地址的严格高阶位,事情就会简单得多。

  1. X86有一组非常非常有限的通用寄存器

  2. 它提倡一种非常低效的底层开发方式(CISC 地狱) ,而不是一种高效的装载/存储方法

  3. 英特尔做出了一个可怕的决定: 引入一个非常愚蠢的段/偏移内存寻址模型来保持兼容性(现在已经是这样了!)过时的技术

  4. 当时大家都在使用32位,而 x86只有区区的16位(其中大多数是8088,即使只有8位外部数据路径,这就更可怕了) ,因此阻碍了主流 PC 世界的发展中央处理器


对我来说(我是一个 DOS 老手,从开发者的角度看过每一代 PC!)第三点。是最糟糕的。

想象一下我们在90年代初的情况(主流!) :

A)由于遗留原因(640kB 容易访问的 RAM)而受到严重限制的操作系统-DOS

B)操作系统扩展(Windows) ,可以在内存方面做得更多,但在游戏等方面受到限制。.并不是地球上最稳定的东西(幸运的是,这种情况后来改变了,但我说的是90年代早期)

C)大多数软件仍然是 DOS,我们不得不经常为特殊软件创建启动磁盘,因为有些程序喜欢 EMM386.exe,有些程序讨厌(特别是游戏玩家——我当时是 AVID 游戏玩家——知道我在说什么)

D)我们被限制在 MCGA 320x200x8位(好吧,有一些特殊的技巧,360x480x8是可能的,但只有在没有运行时库支持的情况下) ,其他一切都是混乱和可怕的(“ VESA”-lol)

E)但是在硬件方面,我们有32位机器,内存和 VGA 卡有几兆字节,支持高达1024x768

Reason for this bad situation?

英特尔的一个简单设计决策。指令级别(非二进制级别!)与已经死亡的东西的兼容性,我认为是8085。其他看似无关的问题(图形模式等)。.)由于技术原因和 x86平台本身所带来的非常狭隘的架构。

今天,情况有所不同,但问问任何汇编程序开发人员或为 x86构建编译器后端的人员就知道了。通用寄存器数量极少,这无异于一个可怕的性能杀手。

我在这里还有一些其他方面:

考虑操作“ a = b/c”x86将这样实现

  mov eax,b
xor edx,edx
div dword ptr c
mov a,eax

作为 div 指令的附加值,edx 将包含其余部分。

RISC 处理器首先需要加载 b 和 c 的地址,将 b 和 c 从内存加载到寄存器,进行除法并加载 a 的地址,然后存储结果。Dst,src 语法:

  mov r5,addr b
mov r5,[r5]
mov r6,addr c
mov r6,[r6]
div r7,r5,r6
mov r5,addr a
mov [r5],r7

这里通常不会有余数。

如果要通过指针加载任何变量,这两个序列都可能变长,尽管对于 RISC 来说这种可能性较小,因为它可能已经在另一个寄存器中加载了一个或多个指针。X86的寄存器较少,因此指针出现在其中一个寄存器中的可能性较小。

Pros and cons:

RISC 指令可能与周围的代码混合以提高指令排程,但 x86不太可能在 CPU 内部完成这项工作(或多或少取决于顺序)。上面的 RISC 序列在32位体系结构上通常是28字节长(每个32位/4字节宽的7条指令)。这将导致芯片外存储器在获取指令时工作得更多(7次获取)。密度更大的 x86序列包含的指令更少,虽然它们的宽度不同,但您可能看到的平均值也是4字节/指令。即使您有指令缓存来加速这个过程,7次提取也意味着与 x86相比,您在其他地方将有3次缺口需要弥补。

用于保存/恢复的寄存器较少的 x86体系结构意味着它可能比 RISC 更快地执行线程切换和处理中断。要保存和恢复更多寄存器,需要更多的临时 RAM 堆栈空间来执行中断,需要更多的永久堆栈空间来存储线程状态。这些方面应该使 x86成为运行纯 RTOS 的更好候选者。

从更个人的角度来说,我发现编写 RISC 汇编比编写 x86更加困难。我用 C 编写 RISC 例程,编译和修改生成的代码来解决这个问题。从代码生产的角度来看,这样做更有效,从执行的角度来看,效率可能更低。所有这32个收银机,保持跟踪。对于 x86,情况正好相反: 6-8个带有“真实”名称的寄存器使问题更容易管理,并且使人们更加相信所生成的代码能够按照预期的方式工作。

丑陋? 那是旁观者的看法,我更喜欢“与众不同”

我认为这个问题有一个错误的假设。主要是痴迷于 RISC 的学者认为 x86很丑。实际上,x86 ISA 可以在一个指令操作中完成,这个操作需要 RISC ISA 上的5-6条指令。RISC 的粉丝可能会反驳说,现代的 x86 CPU 将这些“复杂”的指令分解成微操作系统; 然而:

  1. 在许多情况下,这只是部分正确或者根本不正确。X86中最有用的“复杂”指令是像 mov %eax, 0x1c(%esp,%edi,4)这样的东西,即寻址模式,它们不会被分解。
  2. 在现代机器上,更重要的不是花费的周期数(因为大多数任务不受 CPU 限制) ,而是代码对指令缓存的影响。5-6个固定大小(通常是32位)的指令对缓存的影响远远超过一个很少超过5字节的复杂指令。

大约10-15年前,x86确实吸收了 RISC 的所有优点,而 RISC (实际上是 定义指令集——最小指令集)的剩余特性是有害和不受欢迎的。

Aside from the cost and complexity of manufacturing CPUs and their energy requirements, x86 is 最好的 ISA. Anyone who tells you otherwise is letting ideology or agenda get in the way of their reasoning.

On the other hand, if you are targetting embedded devices where the cost of the CPU counts, or embedded/mobile devices where energy consumption is a top concern, ARM or MIPS probably makes more sense. Keep in mind though you'll still have to deal with the extra ram and binary size needed to handle code that's easily 3-4 times larger, and you won't be able to get near the performance. Whether this matters depends a lot on what you'll be running on it.