如果寄存器如此快速,为什么我们不能拥有更多?

Xeo*_*Xeo 85 performance assembly history cpu-registers

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

Joh*_*ley 116

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

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

这些天我们确实有很多寄存器 - 它们只是没有明确编程.我们有"注册重命名".虽然您只访问一个小集(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]
Run Code Online (Sandbox Code Playgroud)

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]
Run Code Online (Sandbox Code Playgroud)

我认为x86最近有大量重命名的寄存器(ballpark 256).这意味着每个指令只有8位乘以2只是为了说明源和目的地是什么.它将大量增加核心所需的导线数量及其尺寸.因此,大多数设计人员已经确定了16-32个寄存器的最佳位置,对于无序CPU设计,寄存器重命名是缓解它的方法.

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

因此,寄存器库大小有一个自然的最佳位置,对于大多数类CPU来说,它最多可以容纳32个架构寄存器.x86-32有8个寄存器,它肯定太小了.ARM配备了16个寄存器,这是一个很好的折衷方案.如果有的话,32个寄存器有点太多了 - 你最终不需要最后10个寄存器.

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

  • 很好的答案 - 我想在混合中引入另一个原因 - 一个人拥有的寄存器越多,在上下文切换时将它们抛出/从堆栈中拉出的时间就越多.绝对不是主要问题,而是一个考虑因素. (11认同)
  • @WillA好点.但是,具有大量寄存器的架构可以降低此成本.ABI通常会有大多数寄存器的被调用者保存,所以你只需要保存一个核心集.上下文切换通常很昂贵,与其他所有繁文缛节相比,额外的保存/恢复不会花费太多.SPARC实际上通过使寄存器库成为存储区域上的"窗口"来解决这个问题,所以它有点扩展(有点手动). (7认同)
  • 考虑一下如此彻底的答案让我的思绪深受其害,我肯定没想到.另外,感谢我们为什么不需要那么多命名寄存器的解释,这非常有趣!我非常喜欢阅读你的答案,因为我对"引擎盖下"的内容完全感兴趣.:)在接受答案之前我会再等一下,因为你永远都不会知道,但我的+1确定无疑. (4认同)

Dig*_*oss 11

我们不要有更多的人

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

但事实证明,由于寄存器重命名,我们确实有很多寄存器,我们甚至不需要保存它们.CPU实际上有许多寄存器集,它会在代码执行时自动在它们之间切换.这样做纯粹是为了让你获得更多的寄存器.

例:

load  r1, a  # x = a
store r1, x
load  r1, b  # y = b
store r1, y
Run Code Online (Sandbox Code Playgroud)

在仅具有r0-r7的体系结构中,CPU可以自动重写以下代码:

load  r1, a
store r1, x
load  r10, b
store r10, y
Run Code Online (Sandbox Code Playgroud)

在这种情况下,r10是一个隐藏的寄存器,暂时替换r1.CPU可以告诉第一个存储后r1的值永远不会再次使用.这允许第一负载被延迟(甚至片上高速缓存命中通常需要几个周期),而不需要第二负载或第二存储的延迟.