我最近对理解底层计算感兴趣。我了解当今使用广泛的计算机遵循x86 / x86-64体系结构。
据我了解,架构,更具体地说是指令集架构(ISA)是程序员可以发布给CPU的一组指令。
第一个问题是ISA是不断发展还是保持不变?
我认为它一直在发展(意味着新指令不断在增加/修改以前的指令?),然后旧处理器如何执行用新指令编写的代码? (它不知道新的指令,但是应该能够执行代码,因为它具有x86体系结构)。编译器会处理此事还是处理器?基本上,相同的指令集如何能够在所有新旧处理器上运行?
最后,除了微体系结构(这不是程序员关心的问题(如果我错了,请纠正我))之外,程序员在处理新处理器时会看到哪些变化?由于微体系结构的变化,由于有效的执行,旧指令可能会快速运行。但是,是否引入了新的说明以允许以前无法执行的操作?还是以前可以用一堆指令来做,但是现在由于硬件的改变可以用一个来完成?新的寄存器?还有什么?
它是否执行了类似的操作-如果处理器支持此功能强大的新指令以加快执行速度,则使用新指令,否则回退到较慢的旧指令。如果是,谁执行此if-else子句?编译器?如果没有,那会发生什么?
答案 0 :(得分:6)
与大多数ISA一样,x86也在不断发展。
一些ISA通过重新定义现有的操作码来破坏向后兼容(例如MIPS64r6这样做了),但这很少见。例如MIPS32r6 / MIPS64r6就是一个例子:https://en.wikipedia.org/wiki/MIPS_architecture#MIPS32/MIPS64_Release_6重新定义了几种编码,并删除了一些指令。
x86从来没有 向后兼容:Ryzen或Skylake-X仍然可以启动并运行可在8086上运行的机器代码。这就是成为x86 CPU的含义的一部分:另请参见The start of x86: Intel 8080 vs Intel 8086?。 (我们只是在谈论机器代码,但是,如果您以传统BIOS模式而不是UEFI引导PC,即使I / O设备也会被仿真,因此,像早期DOS这样的8086 PC早期操作系统可能实际上是在本地运行的。)
英特尔和AMD将此推到了极致,以至于在当前CPU上仍支持16位和32位模式的未公开文件 8086指令(例如SALC(例如sbb al,al
,但未更新FLAGS)) ,占用了宝贵的操作码编码空间,该空间可用于对新指令进行较短的编码。
但是使用新insns的SW仅适用于新的HW。新软件将在当前和将来的硬件上运行,而旧硬件则选择与之兼容。 (例如,在32位代码中,您可能避免使用cmov
或Pentium Pro新增的其他说明,因此您的代码可以在P5(i586)Pentium / PMMX上运行。)
x86-64设置了一个包含SSE2和PPro指令(例如cmov
)的新基线。因此,幸运的是64位代码不必担心与没有这些功能的旧CPU兼容,而x86-64则需要它们。
一个包含AVX2,FMA和BMI2(例如Haswell)的新基准将非常不错。如果您的编译器可以在整个代码中的任何地方都使用它们来获取更有效的可变计数移位指令,则BMI1 / BMI2尤其有用,而不仅仅是像SIMD指令那样在几个热循环中使用。但是英特尔仍在销售没有BMI2的新CPU(例如,奔腾/赛扬版本的Skylake / Coffee Lake)。
如果没有,那会发生什么?
CPU不支持的指令通常会以#UD
(未定义)出现错误。在类似Unix的操作系统上,您的进程将收到SIGILL(非法指令信号。
制作一个将利用新指令但不会在旧CPU上触发非法指令错误的二进制文件的唯一方法是执行运行时CPU检测和动态分配。一些编译器可以为您做到这一点。
新指令的编码可能(在旧CPU上)看起来像是另一条指令的冗余前缀。例如lzcnt
在不支持它的CPU上将解码为rep bsr
,它仅以bsr
运行。并给出与lzcnt
不同的结果!
(Intel的文档明确指出,不能保证将来的CPU能够以与当前CPU相同的方式来解码具有无意义前缀的指令。这为它们留出了进行ISA扩展的空间。)
有时,对旧的CPU进行无意义的REP前缀静默忽略对于ISA扩展很有用。例如pause
是rep nop
。它在旧CPU上进行无害解码非常有用,可以将其放置在自旋循环中而无需检查。同样,硬件锁省略(事务性存储器)解码为仍可在旧CPU上运行的代码,实际上是执行原子操作而不是开始事务。
另请参见:https://www.agner.org/optimize/blog/read.php?i=25停止Agner Fog的指令集之战。英特尔过去一直不发布即将推出的ISA扩展的详细信息而困扰AMD的历史,因此AMD最终开发了自己不兼容的ISA扩展,并花费了多年的时间才能为对自己CPU的新扩展添加支持。 (例如,在Bulldozer之前,SSSE3在AMD CPU上不可用,这意味着多年以来,即使是需要新型计算机的游戏也无法将其作为基准。)
但是是否引入了新的说明以允许以前无法执行的操作?
是的,SIMD是最重要的示例之一。 MMX,然后是SSE / SSE2,然后是SSE4.x。然后是AVX两倍宽的向量。与一次字节循环相比,并行处理整个16或32字节数据的矢量可以大大加快strlen
或memcmp
之类的处理速度。对于很多数组内容也很有帮助。
AVX2 what is the most efficient way to pack left based on a mask?是由新指令集启用的新技巧的有趣示例。例如AVX512内置了此操作,而AVX2 + BMI2允许使用pdep
/ pext
进行以前无法实现的技巧。
SSSE3 pshufb
是第一个变量控制随机播放指令,从查找表中加载随机播放控件可以使以前不可能的事情变得高效。例如Fastest way to get IPv4 address from string。
How to implement atoi using SIMD?还显示了x86的pmaddubsw
/ pmaddwd
整数乘法+水平加法指令可以与小数位值相乘的一些漂亮操作。
NASM手册的旧版本in an appendix中很好地记录了8086之后添加新指令的早期历史。本附录的当前版本删除了每条指令的文本描述,以便为SIMD指令腾出空间。 (有很多。)
A.77 IMUL: Signed Integer Multiply
IMUL r/m8 ; F6 /5 [8086]
IMUL r/m16 ; o16 F7 /5 [8086]
IMUL r/m32 ; o32 F7 /5 [386]
IMUL reg16,r/m16 ; o16 0F AF /r [386]
IMUL reg32,r/m32 ; o32 0F AF /r [386]
IMUL reg16,imm8 ; o16 6B /r ib [286]
IMUL reg16,imm16 ; o16 69 /r iw [286]
IMUL reg32,imm8 ; o32 6B /r ib [386]
IMUL reg32,imm32 ; o32 69 /r id [386]
IMUL reg16,r/m16,imm8 ; o16 6B /r ib [286]
IMUL reg16,r/m16,imm16 ; o16 69 /r iw [286]
IMUL reg32,r/m32,imm8 ; o32 6B /r ib [386]
IMUL reg32,r/m32,imm32 ; o32 69 /r id [386]
当然,任何reg32指令都需要386才能进行32位扩展,但是请注意,imul-immediate是286(imul cx, [bx], 123
)中的新功能,而2-operand imul是386({{1} }),允许在不破坏DX:AX的情况下进行乘法运算,从而使AX的“特殊性”降低。
其他{386} imul cx, [bx]
和movsx
等386指令在使寄存器更正交方面也走了很长一段路,使您可以有效地将符号扩展到任何寄存器中。在此之前,您必须将数据放入AL中并使用movzx
,或者将AX用于cbw
来将扩展签名为DX:AX。