为什么x86很丑? 与其他人相比,为什么被认为是劣等的?

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

  • 为什么我们需要不同的CPU架构的服务器和迷你/大型机和混合核心? 说
    PC架构是一团糟,任何OS开发者都会告诉你的。

  • 学习汇编语言是否值得努力? ( 存档 )说
    意识到x86架构至多是可怕的

  • 学习x86汇编程序的简单方法是什么? 说
    大多数大学都会在像MIPS这样的东西上进行汇编,因为它比较容易理解,x86汇编真的很难看

还有更多的评论

  • “与大多数架构相比,X86糟透了。”

  • 这绝对是X86不如MIPS,SPARC和PowerPC的传统智慧

  • x86很难看

我尝试search,但没有find任何理由。 我不认为x86可能是坏的,因为这是我熟悉的唯一架构。

有人可以友好地给我考虑x86相比其他人的丑陋/坏/劣等原因。

几个可能的原因:

  1. x86是一个比较老的ISA (毕竟它的祖先是8086)
  2. x86已经进化了很多次,但是硬件需要与旧的二进制文件保持向后兼容。 例如,现代x86硬件仍然支持原生运行16位代码。 此外,存在几种存储器寻址模式,以允许较旧的代码在同一个处理器上互操作,例如实模式,保护模式,虚拟8086模式和(amd64)长模式。 这可能会让一些人感到困惑。
  3. x86是一个CISC机器。 很长一段时间,这意味着它比RISC机器如MIPS或ARM要慢,因为指令具有数据相互依赖性,并且标志使大多数forms的指令级并行性难以实现。 现代实现将x86指令翻译成被称为“ 微操作 ”的类RISC指令,以使这些优化实现在硬件中实现。
  4. 在某些方面,x86并不逊色,只是不同而已。 例如,input/输出在绝大多数体系结构上都作为内存映射进行处理,而在x86上则不然。 (注意:现代x86机器通常具有某种forms的DMA支持,并通过内存映射与其他硬件进行通信;但是ISA仍然具有INOUT等I / O指令)
  5. x86 ISA有很less的体系结构寄存器,可以强制程序更频繁地往返内存。 执行此操作所需的额外指令将花费在有用的工作上的执行资源,尽pipe高效的存储转发可以保持较低的延迟。 将寄存器重命名为大型物理寄存器文件的现代实现可以保留许多指令,但缺乏体系结构寄存器仍然是32位x86的重大缺陷。 x86-64从8位增加到16位整数和向量寄存器是64位代码中比32位更快(以及更高效的寄存器调用ABI)的最大因素之一,而不是每个寄存器的宽度增加。 从16到32个整数寄存器的进一步增加将有助于一些,但不是那么多。 (AVX512增加到32个向量寄存器,因为浮点代码有更高的延迟,并且常常需要更多的常量。)( 见注释 )
  6. x86汇编代码很复杂,因为x86是一个具有许多特性的复杂体系结构。 一个典型的MIPS机器的指令列表适合于单个字母大小的纸张。 x86的等效列表填写了多个页面,说明只是做了更多的工作,所以您经常需要比列表可以提供更多的解释。 例如, MOVSB指令需要一个相对较大的C代码块来描述它的function:

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

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

    尽pipeMIPS(以及类似的体系结构)的简单性并不一定会使它们变得更加优秀,但是对于汇编程序类的介绍,从简单的ISA开始就是有意义的。 一些汇编类教导了一个被称为y86的超简化的x86子集,这个子集被简化为不能用于实际应用(例如,没有移位指令),或者只是教授基本的x86指令。

  7. x86使用可变长度的操作码,这就增加了指令parsing的硬件复杂度。 在现代,随着内存带宽越来越受限于内存带宽,CPU的成本越来越小,但许多“x86抨击”的文章和态度来自这个成本相对较大的时代。
    2016年更新:Anandtech发布了关于x64和AArch64下操作码大小的讨论 。

编辑:这不应该是一个bash的x86! 派对。 考虑到问题的措辞,我别无select,只能做一些抨击。 但是除了(1)之外,所有这些事情都是有原因的(见评论)。 英特尔的devise师并不愚蠢 – 他们希望用自己的架构来完成一些事情,而这些是他们为了使这些事情成为现实而必须付出的一些税收。

在我脑海里,主要的打击x86是CISC的起源 – 指令集包含了很多隐含的相互依赖关系。 这些相互依赖性使得难以在芯片上进行重新sorting,因为每个指令都必须保留这些相互依赖关系的工件和语义。

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

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

DEC Alpha AXP芯片是一种MIPS风格的RISCdevise,在可用的指令中很痛苦,但指令集的devise是为了避免指令间的隐式寄存器依赖性。 没有硬件定义的堆栈寄存器。 没有硬件定义的标志寄存器。 即使指令指针是OS定义的 – 如果你想返回给调用者,你必须弄清楚调用者如何让你知道要返回的地址。 这通常由OS调用约定来定义。 但在x86上,它是由芯片硬件定义的。

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

在x86系列树中,没有看到这种性能提升的情况,这很大程度上是因为x86指令集的复杂性使得许多types的执行优化如果不是不可能,也是非常昂贵的。 英特尔的天才们正在放弃在硬件上实现x86指令集 – 所有现代x86芯片实际上都是RISC内核,在一定程度上解释了x86指令,将它们转换成内部微代码,保留了原始x86的所有语义指令,但是允许微码上的RISC乱序和其他优化的一点点。

我写了很多x86汇编程序,可以充分体会到它的CISC根源的便利性。 但是,我并没有完全理解x86是多么复杂,直到我花了一些时间写出Alpha AXP汇编器。 我被AXP的简单性和统一性所吸引。 差异是巨大的,深刻的。

x86架构可以从8008微处理器和亲属的devise中获得。 这些CPU是在内存很慢的时候devise的,如果你可以在CPU上做,它往往快得多。 但是,CPU裸片空间也很昂贵。 这两个原因是为什么只有less量的寄存器往往有特殊的用途,而且复杂的指令集有各种各样的问题和局限性。

来自同一时代的其他处理器(例如6502系列)也具有类似的限制和怪癖。 有趣的是,8008系列和6502系列都是embedded式控制器。 即使在那个时候,embedded式控制器也需要用汇编语言来编程,而且在很多方面都是为了汇编程序员而不是编译器。 (请看VAX芯片,以满足编译器写入时会发生的情况)devise人员并没有期望他们成为通用计算平台, 这就是POWER巨人前辈们的所为。 当然,家用电脑革命也改变了这一点。

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

考虑操作“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 

这里通常不会有剩余。

如果要通过指针加载任何variables,那么这两个序列可能变得更长,尽pipeRISC的可能性较小,因为它可能已经将一个或多个指针加载到另一个寄存器中。 x86具有较less的寄存器,因此指针在其中一个中的可能性较小。

优点和缺点:

RISC指令可能与周围的代码混合在一起,以改善指令调度,这对x86来说是不太可能的,而在CPU本身内部,这种做法或多或less地取决于序列。 上面的RISC序列通常是32位体系结构中的28个字节(每个32位/ 4个字节宽度的7条指令)。 这会在读取指令(七次读取)时使片外存储器工作得更多。 更密集的x86序列包含更less的指令,虽然它们的宽度有所不同,但您也可能在平均4字节/指令。 即使你有指令高速caching来提高速度,七个提取意味着你将有其他三个赤字来弥补与x86相比的不足。

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

更个人的看法是,我发现编写RISC程序集比x86更困难。 我通过在C中编写RISC例程来解决这个问题,编译和修改生成的代码。 从代码生成的angular度来看,这是更高效的,从执行的angular度来看可能效率更低。 所有这32个寄存器都要跟踪。 对于x86来说,反过来也是如此:具有“真实”名称的6-8个寄存器使得问题更容易pipe理,并且使得所生成的代码能够按照预期工作,从而使人更有信心。

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

我认为这个问题有一个错误的假设。 主要是那些把RISC称为“丑陋”的学者。 实际上,x86 ISA可以在一个单指令操作中完成,在RISC ISA上需要5-6个指令。 RISC爱好者可能会反驳说,现代的x86 CPU将这些“复杂”的指令分解成microops; 然而:

  1. 在许多情况下,这只是部分真实或不真实。 x86中最有用的“复杂”指令就像mov %eax, 0x1c(%esp,%edi,4)即寻址模式,这些不会被细分。
  2. 现代机器上通常更重要的不是所花费的周期数(因为大多数任务不是CPU绑定的),而是指令caching对代码的影响。 5-6个固定大小(通常是32位)的指令将会比一个很less超过5个字节的复杂指令影响caching。

x86在10 – 15年前确实吸收了RISC的所有优点,其余的RISC(实际上是定义的 – 最小的指令集)是有害的,也是不可取的。

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

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

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

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

(幸运的是,32位模式更加安全(虽然有时候本身有点奇怪 – 例如分割),而16位x86代码在引导加载程序和一些embedded式环境之外已经不再那么重要了。

还有一些旧时代的剩菜,当时英特尔正在努力让x86成为最终的处理器。 指令几个字节长,执行的任务,实际上没有人再做任何事情,因为他们坦率地说,太吓人慢或复杂。 对于两个例子,ENTER和LOOP指令 – 注意C栈帧代码就像大多数编译器的“push ebp; mov ebp,esp”而不是“enter”。

我不是一个专家,但似乎很多人不喜欢它的特征可能是它performance良好的原因。 几年前,有了注册(而不是堆栈),注册框架等等,被认为是使架构看起来更简单的好方法。 然而,现在,重要的是caching性能,x86的变长字允许它在caching中存储更多的指令。 我相信对手指出的“指令解码”占用了一半的芯片,现在已经不是那么简单了。

我认为并行是当今最重要的因素之一 – 至less对于已经运行得足够快的algorithm来说是可用的。 在软件中performance出高度的并行性,可以使硬件缓冲(或经常完全隐藏)内存延迟。 当然,进一步实现架构的未来可能就像量子计算。

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

如果您尝试编写一个针对x86的编译器,或者如果您编写一个x86机器模拟器,或者即使您试图在硬件devise中实现ISA,我想也会得到部分答案。

虽然我明白“x86很丑”! 论点,我仍然认为写x86集成比MIPS更有趣 – 例如,后者只是简单的乏味。 它总是意味着编译器而不是人类。 我不确定一个芯片是否会对编译器作者更为敌视

对我来说最丑的部分是实模式分割的方式 – 任何物理地址都有4096个分段:偏移量别名。 最后一次你需要吗? 如果段部分严格是32位地址的高阶位,那么情况会简单得多。

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

  • x86-16有一个相当奇怪的内存寻址scheme ,它允许一个存储单元以多达4096种不同的方式寻址,有限的RAM到1 MB,强制程序员处理两种不同大小的指针。 幸运的是,向32位移动使这个function是不必要的,但是x86芯片仍然带有段寄存器的残余。
  • 尽pipex86 本身并不是一个错误,但x86调用约定并不像MIPS那样标准化(主要是因为MS-DOS没有配备任何编译器),给我们留下了__cdecl__stdcall __cdecl__stdcall __fastcall等的混乱。
  1. x86 has a very, very limited set of general purpose registers

  2. it promotes a very inefficient style of development on the lowest level (CISC hell) instead of an efficient load / store methodology

  3. Intel made the horrifying decision to introduce the plainly stupid segment / offset – memory adressing model to stay compatible with (at this time already!) outdated technology

  4. At a time when everyone was going 32 bit, the x86 held back the mainstream PC world by being a meager 16 bit (most of them – the 8088 – even only with 8 bit external data paths, which is even scarier!) CPU


For me (and I'm a DOS veteran that has seen each and every generation of PCs from a developers perspective!) point 3. was the worst.

Imagine the following situation we had in the early 90s (mainstream!):

a) An operating system that had insane limitations for legacy reasons (640kB of easily accessible RAM) – DOS

b) An operating system extension (Windows) that could do more in terms of RAM, but was limited when it came to stuff like games, etc… and was not the most stable thing on Earth (luckily this changed later, but I'm talking about the early 90s here)

c) Most software was still DOS and we had to create boot disks often for special software, because there was this EMM386.exe that some programs liked, others hated (especially gamers – and I was an AVID gamer at this time – know what I'm talking about here)

d) We were limited to MCGA 320x200x8 bits (ok, there was a bit more with special tricks, 360x480x8 was possible, but only without runtime library support), everything else was messy and horrible ("VESA" – lol)

e) But in terms of hardware we had 32 bit machines with quite a few megabytes of RAM and VGA cards with support of up to 1024×768

Reason for this bad situation?

A simple design decision by Intel. Machine instruction level (NOT binary level!) compatibility to something that was already dying, I think it was the 8085. The other, seemingly unrelated problems (graphic modes, etc…) were related for technical reasons and because of the very narrow minded architecture the x86 platform brought with itself.

Today, the situation is different, but ask any assembler developer or people who build compiler backends for the x86. The insanely low number of general purpose registers is nothing but a horrible performance killer.