【问题标题】:Why didn't Intel made the high order part of their CPUs' registers available?为什么英特尔不提供其 CPU 寄存器的高阶部分?
【发布时间】:2011-07-16 03:10:41
【问题描述】:

在汇编编程和进行某种字符串操作时,我使用alah 有时还有其他人来保存字符,因为这样我可以在寄存器中保存更多数据。我认为这是一个非常方便的功能,但英特尔的工程师似乎不同意我的看法,因为他们没有使寄存器的两个高位字节可访问(或者我错了吗?)。我不明白为什么。我想了一会儿,我的猜测是:

  1. 它们会使 CPU 过于复杂
  2. 它们将毫无用处
  3. 也许是上述两种情况

我想出了第二个,因为我从未见过编译程序(比如使用 gcc)使用 albh 或其中任何一个。

【问题讨论】:

  • gcc 确实使用来自 al / ah 的 movzx,如果您编写的 C 从数组中加载 uint32_t,然后使用移位/与来提取每个字节以用作数组索引。取而代之的是 8 位的 3 次移位,你得到 16 位的 1 次移位。 (或uint64_t 在 64 位模式下,多次移位 16 位。)

标签: assembly x86 cpu-registers


【解决方案1】:

虽然有点笨拙,但您可以将寄存器的一半与rol reg,16(或ror reg,16,如果您愿意)交换。在 Netbust CPU (Pentium IV) 上效率很低,但在大多数较新(或较旧)的 CPU 上,您通常有一个桶形移位器可以在一个时钟内完成。

至于他们为什么不这样做,这很简单:如果他们真的想这样做,就需要彻底重新设计指令编码。在最初的设计中,他们用尽了所有适合他们用来指定寄存器的字段大小的代码。事实上,他们已经使用了一些技巧,其中编码的含义取决于模式,并且如果您需要使用不同的大小,还有地址大小和操作数大小前缀。例如,要在 32 位模式下运行时使用 AX,指令将在指令本身之前有一个操作数覆盖前缀。如果他们真的很想这样做,他们可以扩展这个概念以指定诸如“寄存器 X 的第 16-23 位中的字节”之类的东西,但这会使解码变得更加复杂,并且解码 x86 指令已经相对痛苦.

【讨论】:

    【解决方案2】:

    简短的回答是因为它是如何从 16 位演变而来的。

    Why is there not a register that contains the higher bytes of EAX?

    【讨论】:

    • +1 为您解答。如果没有别的,我会接受的:)
    【解决方案3】:

    除了 Jerry 正确提到的指令编码问题之外,这里还有其他事情在起作用。

    大多数重要的 CPU 都是流水线的:这意味着在普通操作中,指令在先前的指令完成执行之前开始执行。这意味着处理器必须检测一条指令对早期指令的任何依赖关系,并阻止该指令执行,直到它所依赖的数据(或条件标志)可用[1]。

    为寄存器的不同部分命名会使这种依赖关系跟踪变得复杂。如果我写:

    mov  ax,  dx
    add  eax, ecx
    

    那么核心需要知道axeax 的一部分,并且添加应该等到移动的结果可用。这称为部分寄存器更新;虽然看起来很简单,但硬件设计人员通常不喜欢它们,并尽量避免需要跟踪它们(尤其是在现代无序处理器中)。

    为寄存器的高半部分命名会增加一组额外的必须跟踪的部分寄存器名称,这会增加芯片面积和功耗,但几乎没有什么好处。归根结底,这就是 CPU 设计决策的制定方式:芯片面积(和功率)与收益之间的权衡。

    部分寄存器更新并不是唯一会因为为寄存器的高部分命名而变得复杂的事情,但它是最容易解释的事情之一;在现代 x86 CPU 中,还有许多其他小事情需要变得更加复杂才能支持它;综合考虑,额外的复杂性将是巨大的。

    [1] 还有其他解决依赖关系的方法,但为了简单起见,我们在这里忽略它们;他们引入了类似的问题。

    【讨论】:

    • 对于硬件设计师来说,这更像是“幸运 386 没有这样做”。 386 没有单独跟踪部分寄存器的依赖关系,是吗?或者你认为 386 建筑师能看到未来的潜在问题吗?英特尔的 ISA 扩展通常非常短签名。例如cvtsi2ss 不会将寄存器的其余部分归零,因此它具有错误的依赖关系。可能是因为 PIII 将 xmm regs 分成两半,因此需要额外的 uop 才能将上半部分归零。但是现在我们背负着 gcc 发出额外的pxor insns 来破坏依赖关系。
    【解决方案4】:

    补充 Jerry 和 Stephen 迄今为止所说的内容。

    首先想到的是,您必须尽量保守您的操作码/指令编码。从斧头开始,啊,还有人。在使用 eax 提供对高位寄存器的基于字节的访问时是否增加了值(除了已经存在的旋转或移位)?并不真地。如果您正在执行字节操作,为什么要使用 32 位寄存器以及为什么使用高字节?也许利用可用的东西或容忍可用的东西并在其他领域利用以不同的方式优化代码。

    我认为世界上大多数指令集没有这四个名称来表示同一个寄存器是有原因的。而且我不认为是专利在起作用。在当时,它可能是一个很酷的功能或设计。可能起源于将人们从 8 位处理器过渡到这个 8/16 位的东西。无论如何,我认为 al, ah, ax, eax 是糟糕的设计,每个人都从中吸取了教训。正如斯蒂芬提到的那样,您遇到了硬件问题,如果您严格按照直接逻辑来实现它,那将是一团糟,一个多路复用器的老鼠巢将所有东西连接起来(对速度不利,对功率不利),那么您就会进入时机噩梦斯蒂芬正在发生。但是这个指令集有微编码的历史,所以你基本上是在用其他处理器模拟这些指令,并且以同样的方式增加了那个噩梦。明智的做法是将 ax 重新定义为 32 位并摆脱 ah 和 al。从设计的角度来看是明智的,但在可移植性方面是不明智的(对工程有利,对营销、销售等不利)。我认为,旧指令集不限于历史书籍和博物馆的原因(以及其他一些原因)是因为反向兼容性。

    我强烈建议学习许多其他指令集,包括新旧指令集。 msp430、ARM、thumb、mips、6502、z80、PIC(不是 mips 的旧版本)等。仅举几例。看到指令集之间的差异和相似之处是非常有教育意义的 IMO。并且取决于您对理解的深入程度(可变字长与固定长度等),了解我们在进行 16 位到 32 位以及最近的 32 位到 64 位转换时可以为英特尔提供哪些选择,同时努力保持市场份额.

    我认为他们当时选择的解决方案是正确的选择,在通常解码为 16 位操作码的前面插入一个以前未定义的操作码,将其转换为 32 位操作码。或者有时如果没有紧随其后的值(需要知道要阅读多少),则有时不会。它似乎符合当时的指令集。所以又回到了 Jerry 的回答,原因是结合了 8/16 位指令集的设计历史和扩展它的原因。当然,他们可以很容易地使用类似的编码以 ax,ah,al 方式提供对高 16 位的访问,并且他们可以很容易地将四个基址寄存器 A、B、C、D 乘以 8 或 16或 32 个通用寄存器(A、B、C、D、E、F、G、H、...),同时保持反向兼容。

    【讨论】:

      【解决方案5】:

      事实上,传统的 x86 操作码允许选择操作数大小(有时作为特定指令编码,有时通过前缀字节)和寄存器编号选择位。对于寄存器选择,指令编码中总是有三位。这允许总共有八个寄存器。

      原来有四个,16bit的AX/BX/BP/SP,8bit的AL/AH/BL/BH。

      再添加两个得到 CX/DX 加上 CL/CH/DL/DH。没有更多的 8 位寄存器,但在 16 位的寄存器选择中仍有两个未使用的值。

      这在英特尔架构的另一个版本中由索引 regs DI/SI 提供。

      完成后,他们已经用尽了 3 个寄存器选择位(并且无法为 SI/DI/BP/SP 提供 8 位寄存器)。

      因此,AMD64 64 位模式设法使寄存器集翻倍的方式是使用前缀字节(“使用新的 regs”-前缀),类似于传统 x86 代码在 16 位和 32 位操作之间选择的方式。同样的方法用于提供“传统上”没有的 8 位寄存器,即SP/BP/SI/DI

      为了说明,例如,请参阅以下指令编码:

      0:     00 c0                add    %al,%al
      2:     00 c1                add    %al,%cl
      4:     00 c2                add    %al,%dl
      6:     00 c3                add    %al,%bl
      8:     00 c4                add    %al,%ah
      a:     00 c5                add    %al,%ch
      c:     00 c6                add    %al,%dh
      e:     00 c7                add    %al,%bh
      10: 40 00 c4                add    %al,%spl
      13: 40 00 c5                add    %al,%bpl
      16: 40 00 c6                add    %al,%sil
      19: 40 00 c7                add    %al,%dil
      

      并且,对于 [16bit / 64bit] / 32bit,并排,因为它是如此说明性:

      0   : [66/48] 01 c0     add   %?ax,%?ax
      2/3 : [66/48] 01 c1     add   %?ax,%?cx
      4/6 : [66/48] 01 c2     add   %?ax,%?dx
      6/9 : [66/48] 01 c3     add   %?ax,%?bx
      8/c : [66/48] 01 c4     add   %?ax,%?sp
      a/f : [66/48] 01 c5     add   %?ax,%?bp
      c/12: [66/48] 01 c6     add   %?ax,%?si
      e/15: [66/48] 01 c7     add   %?ax,%?di
      

      前缀 0x66 标记 16 位操作,0x48 是 64 位操作的前缀字节之一(如果您的目标和/或源是“新”高之一-编号的寄存器)。

      回到你原来的问题,如何访问高位;好吧,较新的 CPU 具有用于此目的的 SSE 指令;向量寄存器的每个 8/16/32/64 位字段都可以通过例如单独访问shuffle 指令,事实上,Intel / AMD 现在在他们的优化库中提供的许多字符串操作代码不再使用普通的 CPU 寄存器,而是使用向量寄存器。如果您需要较大值的上/下半部分(或其他部分)之间对称,请使用向量寄存器。

      【讨论】:

      • thx ;-) 顺便说一句,以防万一有人想知道:0x40 0x00 0xc00x00 0xc0,在 64 位模式下,都是同一指令的编码,add %al, %al。 AMD 使新方案非常干净。
      猜你喜欢
      • 2020-10-23
      • 1970-01-01
      • 2012-09-28
      • 2017-05-25
      • 2017-07-02
      • 2013-07-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多