【问题标题】:If registers are so blazingly fast, why don't we have more of them?如果寄存器速度如此之快,为什么我们不拥有更多呢?
【发布时间】:2011-08-30 01:40:04
【问题描述】:

在 32 位中,我们有 8 个“通用”寄存器。使用 64 位,数量翻倍,但它似乎独立于 64 位更改本身。
现在,如果寄存器如此之快(没有内存访问),为什么没有更多的自然呢? CPU 构建器不应该在 CPU 中工作尽可能多的寄存器吗?为什么我们只有我们拥有的数量的逻辑限制是什么?

【问题讨论】:

  • CPU 和 GPU 主要通过缓存和大规模多线程来隐藏延迟。因此,CPU 有(或需要)很少的寄存器,而 GPU 有数万个寄存器。请参阅我的survey paper on GPU register file,其中讨论了所有这些权衡和因素。

标签: performance history cpu-registers assembly


【解决方案1】:

您没有大量寄存器的原因有很多:

  • 它们与大多数流水线阶段高度相关。对于初学者,您需要跟踪他们的生命周期,并将结果转发回之前的阶段。复杂性很快变得难以处理,所涉及的电线数量(字面意思)以相同的速度增长。它在面积上很昂贵,这最终意味着在某个时间点之后它在功率、价格和性能上都会很昂贵。
  • 占用指令编码空间。 16 个寄存器占用 4 位用于源和目标,如果您有 3 操作数指令(例如 ARM),则占用另外 4 位。仅仅为了指定寄存器就占用了大量的指令集编码空间。这最终会影响解码、代码大小以及复杂性。
  • 还有更好的方法可以达到同样的效果...

现在我们确实有很多寄存器 - 它们只是没有明确编程。我们有“注册重命名”。虽然您只能访问一小部分(8-32 个寄存器),但它们实际上得到了一个更大的集合(例如 64-256)的支持。然后 CPU 跟踪每个寄存器的可见性,并将它们分配给重命名的集合。例如,您可以连续多次加载、修改、然后存储到寄存器中,并根据缓存未命中等实际独立执行这些操作中的每一个。在 ARM 中:

ldr r0, [r4]
add r0, r0, #1
str r0, [r4]
ldr r0, [r5]
add r0, r0, #1
str r0, [r5]

Cortex A9 内核会进行寄存器重命名,因此第一次加载到“r0”实际上会转到一个重命名的虚拟寄存器 - 我们称之为“v0”。加载、增量和存储发生在“v0”上。同时,我们还再次对 r0 执行加载/修改/存储,但这将重命名为“v1”,因为这是使用 r0 的完全独立的序列。假设由于缓存未命中,来自“r4”中指针的负载停止。没关系——我们不需要等待“r0”准备好。因为它被重命名了,所以我们可以使用“v1”(也映射到 r0)运行下一个序列 - 也许这是缓存命中,我们刚刚获得了巨大的性能提升。

ldr v0, [v2]
add v0, v0, #1
str v0, [v2]
ldr v1, [v3]
add v1, v1, #1
str v1, [v3]

我认为 x86 近来有大量重命名的寄存器(大约 256 个)。这意味着每条指令都有 8 位乘以 2,只是为了说明源和目标是什么。这将大大增加核心所需的电线数量及其尺寸。因此,在 16 到 32 个寄存器附近有一个最佳位置,大多数设计人员都已经解决了这个问题,对于无序 CPU 设计,寄存器重命名是缓解它的方法。

编辑:乱序执行和寄存器重命名的重要性。一旦你有了OOO,寄存器的数量就没有那么重要了,因为它们只是“临时标签”并且被重命名为更大的虚拟寄存器集。您不希望数字太小,因为很难编写小的代码序列。这对 x86-32 来说是个问题,因为有限的 8 个寄存器意味着很多临时寄存器最终会通过堆栈,并且内核需要额外的逻辑来将读/写转发到内存。如果您没有 OOO,则通常是在谈论小内核,在这种情况下,大寄存器集的成本/性能优势很差。

因此,对于大多数类型的 CPU,寄存器组大小有一个天然的最佳位置,最多可容纳大约 32 个架构寄存器。 x86-32 有 8 个寄存器,它肯定太小了。 ARM 有 16 个寄存器,这是一个很好的折衷方案。如果有的话,32 个寄存器有点太多了 - 你最终不需要最后 10 个左右。

这些都不会涉及您为 SSE 和其他矢量浮点协处理器获得的额外寄存器。这些作为一个额外的集合是有意义的,因为它们独立于整数内核运行,并且不会以指数方式增加 CPU 的复杂性。

【讨论】:

  • 很好的答案 - 我想提出另一个原因 - 一个寄存器越多,上下文切换时将它们放入/从堆栈中拉出所需的时间就越多。绝对不是主要问题,而是一个考虑因素。
  • @WillA 好点。但是,具有大量寄存器的体系结构可以降低此成本。 ABI 通常会保存大多数寄存器的被调用者,因此您只需保存一个核心集。上下文切换通常足够昂贵,与所有其他繁文缛节相比,额外的保存/恢复不会花费太多。 SPARC 实际上通过使寄存器组成为内存区域上的“窗口”来解决这个问题,因此它在某种程度上可以扩展(有点挥手)。
  • 考虑到我肯定没想到这么彻底的答案让我大吃一惊。另外,感谢您对为什么我们真的不需要那么多命名寄存器的解释,这非常有趣!我真的很喜欢阅读您的回答,因为我对“幕后”发生的事情完全感兴趣。 :) 在接受答案之前我会再等一会儿,因为你永远不知道,但我的 +1 是肯定的。
  • 无论保存寄存器的责任在哪里,所花费的时间都是管理开销。好的,所以上下文切换可能不是最常发生的情况,但中断是。手工编码的例程可能会节省寄存器,但如果驱动程序是用 C 编写的,那么中断声明的函数很可能会保存每个寄存器,调用 isr 然后恢复所有保存的寄存器。与 RISC 架构的 32+ 一些 regs 相比,IA-32 的 15-20 regs 具有中断优势。
  • 很好的答案,但我不同意将“重命名”寄存器与“真实”可寻址寄存器进行直接比较。在 x86-32 上,即使有 256 个内部寄存器,您也不能在任何单个执行点使用超过 8 个存储在寄存器中的临时值。基本上,寄存器重命名只是OOE的一个奇怪的副产品,仅此而已。
【解决方案2】:

我们确实有更多的人

因为几乎每条指令都必须选择 1、2 或 3 个架构上可见的寄存器,所以增加它们的数量会使每条指令的代码大小增加几位,从而降低代码密度。它还增加了必须保存为线程状态的 context 的数量,并且部分保存在函数的 activation record 中。 这些操作经常发生。流水线互锁必须检查每个寄存器的记分牌,这具有二次时间和空间复杂度。也许最大的原因仅仅是与已经定义的指令集的兼容性。

但事实证明,感谢 register renaming 我们确实有很多可用的寄存器,我们甚至不需要保存它们。 CPU 实际上有许多寄存器集,它会在您的代码执行时自动在它们之间切换。这样做纯粹是为了让您获得更多的寄存器。

例子:

load  r1, a  # x = a
store r1, x
load  r1, b  # y = b
store r1, y

在只有 r0-r7 的架构中,以下代码可能会被 CPU 自动重写为:

load  r1, a
store r1, x
load  r10, b
store r10, y

在这种情况下,r10 是一个隐藏寄存器,临时替代了 r1。 CPU 可以判断 r1 的值在第一次存储之后再也不会被使用。这允许延迟第一次加载(即使片上缓存命中通常需要几个周期),而无需延迟第二次加载或第二次存储。

【讨论】:

    【解决方案3】:

    它们一直在添加寄存器,但它们通常与特殊用途的指令(例如 SIMD、SSE2 等)相关联,或者需要编译到特定的 CPU 架构,这会降低可移植性。现有指令通常在特定寄存器上工作,如果其他寄存器可用,则无法利用它们。旧版指令集等等。

    【讨论】:

      【解决方案4】:

      在这里添加一些有趣的信息,您会注意到拥有 8 个相同大小的寄存器允许操作码保持与十六进制表示法的一致性。例如,指令push ax 是 x86 上的操作码 0x50,最后一个寄存器 di 上升到 0x57。然后指令 pop ax 从 0x58 开始并上升到 0x5F pop di 以完成第一个 base-16。每个大小有 8 个寄存器保持十六进制一致性。

      【讨论】:

      • 在 x86/64 上,REX 指令前缀将寄存器索引扩展为更多位。
      猜你喜欢
      • 2011-03-31
      • 2011-11-23
      • 2019-12-02
      • 1970-01-01
      • 2020-10-01
      相关资源
      最近更新 更多