为什么x86难看?为什么与其他人相比,它被认为是劣等的?

时间:2010-04-21 02:14:46

标签: assembly x86 mips x86-64 cpu-architecture

最近我一直在阅读一些SO档案并遇到针对x86架构的声明。

还有更多评论,比如

我试过搜索但没有找到任何理由。我发现x86不好,可能是因为这是我唯一熟悉的架构。

有人可以给我一些考虑x86丑陋/坏/劣等的理由。

10 个答案:

答案 0 :(得分:85)

有几个可能的原因:

  1. x86是一个相对较老的ISA(毕竟它的祖先是8086s)
  2. x86已经发展了好几次,但硬件需要保持与旧二进制文件的向后兼容性。例如,现代x86硬件仍然支持本机运行16位代码。另外,存在若干存储器寻址模型以允许较旧的代码在同一处理器上互操作,例如实模式,保护模式,虚拟8086模式和(amd64)长模式。这可能让一些人感到困惑。
  3. x86是CISC机器。很长一段时间,这意味着它比像MIPS或ARM这样的RISC机器要慢,因为指令data interdependency and flags使得大多数形式的指令级并行性难以实现。现代实现将x86指令转换为类似RISC的指令,称为“micro-ops”,以使这些优化在硬件中实现。
  4. 在某些方面,x86并不逊色,它只是不同。例如,输入/输出在绝大多数体系结构上作为内存映射处理,但不在x86上处理。 (注意:现代x86机器通常具有某种形式的DMA支持,并通过内存映射与其他硬件进行通信;但ISA仍然具有IN和{{1}等I / O指令}})
  5. x86 ISA只有很少的架构寄存器,可以强制程序比其他方式更频繁地遍历内存。尽管efficient store-forwarding保持低延迟,但执行此操作所需的额外指令会占用可用于有用工作的执行资源。将寄存器重命名为大型物理寄存器文件的现代实现可以保留许多指令,但缺少架构寄存器仍然是32位x86的重要弱点。 x86-64从8变为16整数和向量寄存器是64位代码比32位更快的最大因素之一(以及更高效的寄存器调用ABI),而不是每个寄存器增加的宽度。进一步从16个增加到32个整数寄存器可以帮助一些,但不会那么多。 (但AVX512确实增加到32个向量寄存器,因为浮点代码具有更高的延迟并且通常需要更多常量。)(see comment
  6. x86汇编代码很复杂,因为x86是一个具有许多功能的复杂架构。典型MIPS机器的说明列表适合单个字母大小的纸张。 x86的等效列表填写了几页,而且说明只是做了更多,所以你经常需要一个比列表可以提供的更多的解释。例如,MOVSB instruction需要一个相对较大的C代码块来描述它的作用:

    OUT

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

    虽然MIPS(和类似的体系结构)简单并不一定能使它们更优越,但是对于汇编汇编类的介绍,从更简单的ISA开始是有意义的。一些汇编类教导了一个名为y86的x86的超简化子集,它被简化为超出了对实际使用无用的点(例如没有移位指令),或者一些只教授基本的x86指令。

  7. x86使用可变长度操作码,这增加了解析指令的硬件复杂性。在现代时代,由于CPU变得越来越受内存带宽的限制而不是原始计算,这种成本变得越来越小,但许多“x86抨击”文章和态度来自于这个成本相对大得多的时代。 2016年更新:Anandtech发布了discussion regarding opcode sizes under x64 and AArch64

  8. 编辑:这不应该是 bash x86!派对。考虑到问题的措辞,我别无选择,只能进行一些抨击。但除了(1)之外,所有这些事情都是有充分理由的(见评论)。英特尔设计师并不愚蠢 - 他们希望通过他们的架构来实现一些目标,而这些是他们为实现这些目标而必须付出的一些税收。

答案 1 :(得分:24)

在我看来,对x86的主要打击是它的CISC起源 - 指令集包含许多隐含的相互依赖性。这些相互依赖性使得难以对芯片上的指令重新排序这样做,因为必须为每个指令保留这些相互依赖性的工件和语义。

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

在RISC架构中,add指令将指定输入操作数和输出寄存器,并且仅使用那些寄存器进行有关操作的所有操作。这使得分离彼此接近的添加操作变得更加容易,因为没有bloomin'标志注册强制所有内容排队并执行单个文件。

DEC Alpha AXP芯片是一种MIPS风格的RISC设计,在可用指令中非常简洁,但指令集旨在避免指令间隐式寄存器依赖性。没有硬件定义的堆栈寄存器。没有硬件定义的标志寄存器。甚至指令指针也是操作系统定义的 - 如果你想返回调用者,你必须弄清楚调用者将如何让你知道要返回的地址。这通常由OS调用约定定义。但是在x86上,它是由芯片硬件定义的。

无论如何,超过3代或4代Alpha AXP芯片设计,硬件从具有32个int寄存器和32个浮点寄存器的spartan指令集的文字实现变为具有80个内部寄存器的大规模乱序执行引擎,注册重命名,结果转发(将前一条指令的结果转发给后来依赖于该值的指令)以及各种疯狂和疯狂的性能提升器。所有这些花里胡哨的东西,AXP芯片芯片仍然比当时的奔腾芯片芯片小得多,而AXP的速度要快得多。

在x86系列树中,你没有看到那些突发性能提升的东西很大程度上是因为x86指令集的复杂性使得许多种执行优化成本过高,如果不是不可能的话。英特尔的天才是放弃在硬件上实现x86指令集 - 所有现代x86芯片实际上都是RISC核心,在某种程度上解释了x86指令,将它们转换为内部微码,保留了原始x86的所有语义指令,但允许一小部分RISC无序和其他优化微码。

我编写了很多x86汇编程序,可以充分理解其CISC根本的便利性。但是我没有完全理解x86是多么复杂,直到我花了一些时间编写Alpha AXP汇编程序。我对AXP的简洁和统一感到惊讶。差异是巨大而深刻的。

答案 2 :(得分:19)

x86架构可以追溯到8008微处理器和亲属的设计。这些CPU是在内存很慢的时候设计的,如果你能在CPU死机上完成,那么它通常会更快很多。但是,CPU芯片空间也很昂贵。这两个原因是为什么只有少数寄存器具有特殊用途,以及具有各种陷阱和限制的复杂指令集。

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

答案 3 :(得分:12)

我还有一些其他方面:

考虑操作“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具有较少的寄存器,因此指针在其中一个中的可能性较小。

利弊:

RISC指令可以与周围的代码混合以改进指令调度,这对于x86来说是不太可能的,而x86则在CPU本身内部(或多或少地取决于序列)工作。上面的RISC序列通常在32位架构上长28个字节(7个32位/ 4个字节宽度的指令)。这将导致片外存储器在获取指令时工作更多(七次读取)。更密集的x86序列包含更少的指令,虽然它们的宽度不同,但你可能也会看到平均4字节/指令。即使你有指令缓存加速这七次,也意味着与x86相比,你可以在其他地方减少三个补偿。

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

从更个人的角度来看,我发现编写RISC程序集比编写x86更困难。我通过在C中编写RISC例程来编译和修改生成的代码来解决这个问题。从代码生产的角度来看,这更有效,从执行的角度来看效率可能更低。所有这32个寄存器都要跟踪。对于x86,它是另一种方式:6-8个带有“真实”名称的寄存器使问题更易于管理,并且使得生成的代码能够按预期工作更有信心。

丑?这是旁观者的眼睛。我更喜欢“不同。”

答案 4 :(得分:9)

我认为这个问题有一个错误的假设。它主要是只有RISC痴迷的学者称x86丑陋。实际上,x86 ISA可以在单个指令操作中执行,这将在RISC ISA上执行5-6条指令。 RISC粉丝可能会反驳现代x86 CPU将这些“复杂”指令分解为microops;但是:

  1. 在许多情况下,这只是部分正确或根本不正确。 x86中最有用的“复杂”指令类似于mov %eax, 0x1c(%esp,%edi,4),即寻址模式,这些都没有细分。
  2. 在现代机器上通常更重要的不是花费的周期数(因为大多数任务不是cpu绑定的)而是代码的指令缓存影响。 5-6固定大小(通常是32位)指令会对缓存产生很多影响,而且复制指令很少超过5个字节。
  3. x86在10 - 15年前真正吸收了RISC的所有优点,RISC的其余特性(实际上定义 1 - 最小指令集)是有害且不可取的。

    除了制造CPU及其能源需求的成本和复杂性之外,x86还是最好的ISA 。任何告诉你的人都会让意识形态或议程妨碍他们的推理。

    另一方面,如果您的目标是占用CPU成本的嵌入式设备,或者能源消耗最受关注的嵌入式/移动设备,ARM或MIPS可能更有意义。请记住,虽然你仍然需要处理处理代码所需的额外内存和二进制大小,这些代码很容易大3-4倍,但你将无法接近性能。这是否重要取决于您将在其上运行的内容。

答案 5 :(得分:7)

x86汇编语言并不是那么糟糕。当你到达机器代码时,它开始变得非常难看。指令编码,寻址模式等比大多数RISC CPU复杂得多。为了向后兼容的目的,内置了额外的乐趣 - 只有当处理器处于特定状态时才会启动。

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

(幸运的是,32位模式更加安全(虽然有时候仍然有点奇怪 - 例如分段),并且16位x86代码在引导加载程序和某些嵌入式环境之外已经基本不相关了。)< / p>

还有来自昔日的剩余物,当时英特尔试图让x86成为终极处理器。说明几个字节长,执行任何人实际上不再执行的任务,因为他们坦率地说太慢或者复杂。对于两个例子,输入和LOOP instructions - 注意C堆栈帧代码类似于“push ebp; mov ebp,esp”而不是大多数编译器的“输入”。

答案 6 :(得分:3)

我不是专家,但似乎人们不喜欢它的许多功能可能是它表现良好的原因。几年前,拥有寄存器(而不是堆栈),寄存器帧等被认为是使架构看起来更简单的好方法。但是,现在,重要的是缓存性能,x86的可变长度字允许它在缓存中存储更多指令。我相信对手指出曾经占​​据芯片一半的“指令解码”,已经不再那么多了。

我认为并行性是当今最重要的因素之一 - 至少对于已经运行得足够快且可以使用的算法而言。在软件中表现出高并行性允许硬件分摊(或经常完全隐藏)内存延迟。当然,更远的架构未来可能就像量子计算一样。

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

答案 7 :(得分:3)

如果您尝试编写针对x86的编译器,或者编写x86机器仿真器,或者即使您尝试在硬件设计中实现ISA,我认为您将获得部分答案。< / p>

虽然我明白“x86很难看!”参数,我仍然认为它比有趣编写x86程序集 - 后者只是简单乏味。它总是对编译器而不是人类很好。我不确定芯片是否会对编译器编写者产生更多的敌意......如果它尝试的话......

对我来说,最丑陋的部分是(实模式)分段的工作方式 - 任何物理地址都有4096段:偏移别名。什么时候你需要那个?如果段部分是32位地址的严格高阶位,事情会变得如此简单。

答案 8 :(得分:2)

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

  • x86-16有一个相当奇怪的memory addressing scheme,它允许以多达4096种不同的方式寻址单个内存位置,将RAM限制为1 MB,并强制程序员处理两种不同大小的指针。幸运的是,转向32位使得这个功能变得不必要,但x86芯片仍然带有段寄存器。
  • 虽然不是x86 本身的错误,但是x86调用约定并没有像MIPS那样标准化(主要是因为MS-DOS没有附带任何编译器),让我们陷入困境__cdecl__stdcall__fastcall

答案 9 :(得分:0)

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

  2. 它在最低层次(CISC地狱)而不是有效的加载/存储方法上促进了一种非常低效的开发方式

  3. 英特尔做出了一个可怕的决定,即引入明显愚蠢的段/偏移 - 内存地址模型以保持兼容(此时已经!)过时的技术

  4. 在每个人都是32位的时候,x86只用了微弱的16位(其中大多数是8088 - 甚至只有8位外部数据路径,甚至只有8位)阻止了主流PC世界更可怕!)CPU


  5. 对我来说(我是DOS老手,从开发人员的角度看过每一代PC!)第3点是最糟糕的。

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

    a)由于遗留原因(640kB易于访问的RAM)而具有疯狂限制的操作系统 - DOS

    b)操作系统扩展(Windows)可以在内存方面做得更多,但在游戏等方面受到限制......并且不是地球上最稳定的东西(幸运的是后来发生了变化) ,但我在这里谈论的是90年代初期)

    c)大多数软件仍然是DOS,我们不得不经常为特殊软件创建启动盘,因为有一些程序喜欢这个EMM386.exe,其他人讨厌(特别是游戏玩家 - 我此时是AVID游戏玩家 - 知道我在这里说的是什么)

    d)我们仅限于MCGA 320x200x8位(好吧,有一些特殊技巧,360x480x8可能,但只有没有运行时库支持),其他一切都很混乱和可怕(“VESA” - lol)< / p>

    e)但就硬件而言,我们拥有32位机器,具有相当多的RAM和VGA卡,支持高达1024x768

    这种糟糕情况的原因?

    英特尔的简单设计决策。机器指令级别(非二进制级别!)与已经死亡的东西的兼容性,我认为是8085.另外,看似无关的问题(图形模式等......)由于技术原因和非常狭窄而相关x86平台自带的架构。

    今天,情况有所不同,但要求任何汇编程序开发人员或为x86构建编译器后端的人员。数量极少的通用寄存器只不过是一个可怕的性能杀手。