为什么 MIPS 将 R0 用作“零”,而您只能对两个寄存器进行异或运算以产生 0?

电器工程 中央处理器 计算机架构 mips
2022-01-02 09:56:49

我想我正在寻找一个琐事问题的答案。我试图理解为什么 MIPS 架构在寄存器中使用显式的“零”值,而您只需通过对自身进行异或任何寄存器来实现相同的目的。可以说手术已经为您完成了;但是,我真的无法想象您会使用大量“零”值的情况。我阅读了 Hennessey 的原始论文,事实上它只是分配了一个零,没有任何真正的理由。

是否存在硬编码二进制赋值为零的逻辑原因?

更新: 在 PIC32MZ 中 MIPS 内核的 xc32-gcc 的 8k 可执行文件中,我有一个“零”实例。

add     t3,t1,zero

实际答案: 我将赏金授予了解 MIPS 和条件代码信息的人。答案实际上在于条件的 MIPS 架构。虽然我最初不想为此分配时间,但我审查了opensparcRISC-VMIPS-IV和 OpenPOWER 的架构(本文档是内部文档),这里是总结发现。由于流水线的体系结构,在分支上进行比较所需的 R0 寄存器。

  • 整数与零比较和分支 (bgez,bgtz,blez,bltz)
  • 整数比较两个寄存器和分支 (beq,bne)
  • 整数比较两个寄存器和陷阱 (teq,tge,tlt,tne)
  • 整数比较寄存器和立即数和陷阱 (teqi,tgei,tlti,tnei)

它只是简单地归结为硬件在实现中的外观。在 RISC-V 手册中,第 68 页有一个未引用的引用:

条件分支被设计成包括两个寄存器之间的算术比较操作(在 PA-RISC 和 Xtensa ISA 中也是如此),而不是使用条件代码(x86、ARM、SPARC、PowerPC),或者只比较一个寄存器与零( Alpha、MIPS)或两个仅用于相等的寄存器 (MIPS)。这种设计的动机是观察到组合比较和分支指令 ts 进入常规流水线,避免额外的条件代码状态或使用临时寄存器,并减少静态代码大小和动态指令提取 trac。另一点是与零的比较需要非平凡的电路延迟(尤其是在高级过程中转向静态逻辑之后),因此几乎与算术量级比较一样昂贵。融合比较和分支指令的另一个优点是在前端指令流中更早地观察到分支,因此可以更早地预测。在可以基于相同的条件代码采取多个分支的情况下,具有条件代码的设计可能具有优势,但我们认为这种情况相对较少。

RISC-V 文档没有击中引用部分的作者。我感谢大家的时间和考虑。

4个回答

RISC CPU 上的零寄存器很有用,原因有两个:

这是一个有用的常数

根据 ISA 的限制,您不能在某些指令编码中使用文字,但可以确保可以使用它r0来获得 0。

可用于合成其他指令

这也许是最重要的一点。作为 ISA 设计人员,您可以将通用寄存器换成零寄存器,以便能够综合其他有用的指令。合成指令很好,因为通过减少实际指令,您需要更少的位来对操作码中的操作进行编码,从而释放指令编码空间中的空间。您可以使用该空间来拥有更大的地址偏移量和/或文字。

零寄存器的语义就像/dev/zero在 *nix 系统上一样:写入它的所有内容都被丢弃,并且您总是读回 0。

r0让我们看几个例子,说明如何在零寄存器的帮助下制作伪指令:

; ### Hypothetical CPU ###

; Assembler with syntax:
; op rd, rm, rn 
; => rd: destination, rm: 1st operand, rn: 2nd operand
; literal as #lit

; On an CPU architecture with a status register (which contains arithmetic status
; flags), `sub` can be used, with r0 as destination to discard result.
cmp rn, rm     ; => sub r0, rn, rm

; `add` instruction can be used as a `mov` instruction:
mov rd, rm     ; => add rd, rm, r0
mov rd, #lit   ; => add rd, r0, #lit

; Negate:
neg rd, rm     ; => sub rd, r0, rm

; On CPU without status flags,
nop            ; => add r0, r0, r0

; RISC-V's `jal` instruction -- Jump and Link: Jump to PC-relative instruction,
; save return address into rd; we can synthesize a `jmp` instruction out of it.
jmp dest       ; => jal r0, dest

; You can even load from an absolute (direct) address, for a usually small range
; of addresses by using a literal offset as an address.
ld rd, addr    ; => ld rd, [r0, #addr]

MIPS的案例

我更仔细地查看了 MIPS 指令集。有一些伪指令使用$zero; 它们主要用于分支。以下是我发现的一些示例:

move $rt, $rs          => add $rt, $rs, $zero

not $rt, $rs           => nor $rt, $rs, $zero

b Label                => beq $zero, $zero, Label ; a small relative branch

bgt $rs, $rt, Label    => slt $at, $rt, $rs
                          bne $at, $zero, Label

blt $rs, $rt, Label    => slt $at, $rs, $rt
                          bne $at, $zero, Label

bge $rs, $rt, Label    => slt $at, $rs, $rt
                          beq $at, $zero, Label

ble $rs, $rt, Label    => slt $at, $rt, $rs
                          beq $at, $zero, Label

至于为什么您在反汇编中只找到一个$zero寄存器实例,也许是您的反汇编程序足够聪明,可以将已知的指令序列转换为等效的伪指令。

零寄存器真的有用吗?

好吧,显然,ARM 发现零寄存器足够有用,以至于在他们(有点)实现 AArch64 的新 ARMv8-A 内核中,现在有一个 64 位模式的零寄存器;以前没有零寄存器。(寄存器有点特殊,在某些编码上下文中它是一个零寄存器,在其他情况下它指定堆栈指针

大多数 ARM/POWER/SPARC 实现都有一个隐藏的 RAZ 寄存器

您可能认为 ARM32、SPARC 等没有 0 寄存器,但实际上它们有!在微架构级别,大多数 CPU 设计工程师添加了一个可能对软件不可见的 0 寄存器(ARM 的零寄存器是不可见的),并使用该零寄存器来简化指令解码。

考虑一个典型的现代 ARM32 设计,它有一个软件不可见的寄存器,比如 R16 连接到 0。考虑 ARM32 加载,许多 ARM32 加载指令的情况属于这些形式之一(暂时忽略前后索引以保持讨论简单)...

LDR ra, [rb] // NOTE:The ! is optional and represents address writeback.
LDR ra, [rb, rc](!)
LDR ra, [rb, #k](!)

在处理器内部,这解码为一般

ldr.uop ra, rb, rx, rc, #c // Internal decoded instruction format.

在进入读取寄存器的发布阶段之前。请注意,rx 表示用于写回更新地址的寄存器。以下是一些解码示例:

LDR R0, [R1]      ==> ldr.uop R0, R1, R16, R16, #0 // Writeback to NULL. 
LDR R0, [R1, R2]! ==> ldr.uop R0, R1, R1, R2,   #0 // Writeback to R1.
LDR R0, [R1, #2]  ==> ldr.uop R0, R1, R16, R16, #2 // Writeback to NULL.

在电路级别,所有三个负载实际上都是相同的内部指令,获得这种正交性的一种简单方法是创建一个接地寄存器 R16。由于 R16 始终接地,这些指令自然而然地正确解码,无需任何额外的逻辑。将一类指令映射到单一内部格式极大地有助于超标量实现,因为它降低了逻辑复杂性。

另一个原因是丢弃写入的简化方式。只需将目标寄存器和标志设置为 R16 即可禁用指令。无需创建任何其他控制信号来禁用回写等。

无论架构如何,大多数处理器实现最终都会在流水线的早期使用 RAZ 寄存器模型。MIPS 流水线本质上是从其他架构中的几个阶段开始的。

MIPS 做出了正确的选择

因此,在任何现代处理器实现中,读为零的寄存器几乎都是强制性的,考虑到它如何简化内部解码逻辑,使其对软件可见的 MIPS 绝对是一个加分点。MIPS 处理器的设计者不需要添加额外的 RAZ 寄存器,因为 $0 已经在地面上。由于 RAZ 对汇编器可用,因此 MIPS 可以使用许多伪指令,可以将其视为将部分解码逻辑推送到汇编器本身,而不是为每种指令类型创建专用格式以对软件隐藏 RAZ 寄存器与其他架构一样。RAZ 寄存器是个好主意,这就是 ARMv8 复制它的原因。

如果 ARM32 有一个 $0 寄存器,解码逻辑会变得更简单,并且架构在速度、面积和功耗方面会更好。例如,在上面介绍的三个版本的 LDR 中,只需要两种格式。同样,不需要为 MOV 和 MVN 指令保留解码逻辑。此外,CMP/CMN/TST/TEQ 将变得多余。也不需要区分短乘法(MUL)和长乘法(UMULL/SMULL),因为短乘法可以被认为是长乘法,高位寄存器设置为 $0 等。

由于 MIPS 最初是由一个小团队设计的,设计的简单性很重要,因此本着 RISC 的精神明确选择了 0 美元。ARM32 在架构层面保留了许多传统的 CISC 特性。

Disclamer:我不太了解 MIPS 汇编器,但 0 值寄存器并不是这种架构所独有的,我猜它的使用方式与我知道的其他 RISC 架构中的使用方式相同。

异或寄存器获得 0 将花费您一条指令,而使用预定义的 0 值寄存器则不会。

例如,mov RX, RY指令通常实现为add RX, RY, R0. 如果没有 0 值寄存器,您xor RZ, RZ每次想要使用mov.

另一个例子是cmp指令及其变体(如“比较和跳转”、“比较和移动”等),cmp RX, R0用于测试负数。

在寄存器库的末端将几根引线连接到地面很便宜(比让它成为一个完整的寄存器便宜)。

执行实际的异或需要一些功率和时间来切换门,然后将其存储在寄存器中,当现有的 0 值可以很容易地获得时,为什么要支付这笔费用。

现代 CPU 也有一个(隐藏的)0 值寄存器,它们可以通过寄存器重命名作为xor eax eax指令的结果使用。