您没有大量寄存器的原因有很多:
- 它们与大多数流水线阶段高度相关。对于初学者,您需要跟踪他们的生命周期,并将结果转发回之前的阶段。复杂性很快变得难以处理,所涉及的电线数量(字面意思)以相同的速度增长。它在面积上很昂贵,这最终意味着在某个时间点之后它在功率、价格和性能上都会很昂贵。
- 占用指令编码空间。 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 的复杂性。