如果登记册如此之快,为什么我们没有更多的登记册呢?


88

在32位中,我们有8个“通用”寄存器。使用64位时,数量增加了一倍,但似乎与64位更改本身无关。
现在,如果寄存器是如此之快(无内存访问),为什么自然不存在更多的寄存器呢?CPU建设者不应该在CPU中使用尽可能多的寄存器吗?为什么我们只有数量的逻辑限制是什么?


CPU和GPU分别通过缓存和大规模多线程来隐藏延迟。因此,CPU具有(或需要)很少的寄存器,而GPU具有成千上万的寄存器。请参阅我在GPU寄存器文件上的调查论文,其中讨论了所有这些权衡和因素。
user984260 '16

Answers:


119

有很多原因,您不仅拥有大量的寄存器:

  • 它们与大多数流水线阶段密切相关。对于初学者,您需要跟踪其寿命,并将结果转发回先前的阶段。复杂性变得非常棘手,涉及的电线数量(字面)以相同的速度增长。它在面积上很昂贵,这最终意味着在某个点之后它在功率,价格和性能上都是昂贵的。
  • 它占用指令编码空间。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的操作,但是由于使用r0是完全独立的序列,因此将其重命名为“ v1”。假设由于高速缓存未命中而使来自“ 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设计者来说是一个不错的选择,而对于乱序的CPU设计,寄存器重命名是缓解这种情况的一种方法。

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

因此,对于寄存器库大小来说,自然是一个不错的选择,对于大多数类型的CPU,最大可以容纳约32个架构寄存器。x86-32有8个寄存器,绝对太小了。ARM提供了16个寄存器,这是一个很好的折衷方案。32个寄存器(如果有的话)有点太多-您最终不需要最后10个左右。

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


12
很好的答案-我想提出另一个理由-寄存器越多,上下文切换时将它们扔到栈上/从栈中拉出的时间就越多。绝对不是主要问题,而是一个考虑因素。
将在

7
@Will一个好点。但是,具有大量寄存器的体系结构可以减轻这种成本。ABI通常会将大多数寄存器保存为被调用方,因此您只需要保存一个核心集即可。上下文切换通常非常昂贵,因此与所有其他繁文tape节相比,额外的保存/还原不会花费很多。SPARC实际上通过使寄存器组成为内存区域上的“窗口”来解决此问题,因此它在某种程度上进行了扩展(有点手摇)。
约翰·里普利

4
考虑一下我肯定没有想到的如此彻底的回答使我震惊。另外,感谢您对为什么我们真的不需要那么多命名寄存器的解释,这很有趣!我非常喜欢阅读您的答案,因为我对“幕后”一事完全感兴趣。:)我要再等一会再接受答案,因为您永远不会知道,但是我的+1是可以肯定的。
Xeo

1
不管保存寄存器的责任在哪里,花费的时间都是管理开销。好的,因此上下文切换可能不是最常见的情况,但是中断是。手工编码的例程可以节省寄存器,但是如果使用C语言编写驱动程序,则中断声明的函数将保存每个寄存器,调用isr,然后恢复所有保存的寄存器。与RISC架构的32+某些寄存器相比,IA-32的15-20寄存器具有中断优势。
Olof Forshell

1
很好的答案,但我不同意将“重命名”寄存器与“真实”可寻址寄存器直接进行比较。在x86-32上,即使有256个内部寄存器,您也不能在任何单点执行中使用超过8个存储在寄存器中的临时值。基本上,寄存器重命名只是OOE的一个奇怪产物,仅此而已。
noop 2012年

12

我们不要让更多的人

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

但是事实证明,由于寄存器重命名我们确实确实有很多可用的寄存器,我们甚至不需要保存它们。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的值。这允许延迟第一加载(即使片上缓存命中通常也需要几个周期),而无需延迟第二加载或第二存储。


2

它们一直在增加寄存器,但是它们通常与特殊目的的指令(例如SIMD,SSE2等)联系在一起,或者需要编译为特定的CPU架构,从而降低了可移植性。现有指令通常在特定寄存器上运行,并且如果可用,则无法利用其他寄存器。旧版指令集和所有。


1

要在此处添加一些有趣的信息,您会注意到具有8个相同大小的寄存器使操作码可以保持十六进制表示法的一致性。例如,指令push ax是x86上的操作码0x50,最后一个寄存器di的操作码升至0x57。然后,指令pop ax从0x58开始并上升到0x5F pop di以完成第一个基数16。十六进制一致性由每个大小的8个寄存器保持。


2
在x86 / 64上,REX指令前缀将寄存器索引扩展为更多位。
Alexey Frunze'3
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.