当您可以对两个寄存器进行XOR以产生0时,为什么MIPS将R0用作“零”?


10

我认为我正在寻找琐事问题的答案。我试图理解为什么MIPS体系结构在通过对自己的寄存器进行异或运算就可以实现同一目的时,在寄存器中使用显式的“零”值的原因。可以说该操作已经为您完成;但是,我真的无法想象您会使用很多“零”值的情况。我阅读了轩尼诗的原始论文,事实上,它只是分配了一个零,没有任何正当的理由。

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

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

add     t3,t1,zero

实际答案: 我将赏金授予拥有MIPS和条件代码信息的人。答案实际上取决于条件的MIPS架构。尽管起初我不想分配时间,但我回顾了openparcMIPS-V和OpenPOWER的体系结构(本文为内部文件),以下是摘要。由于管线的体系结构,R0寄存器是在分支上进行比较所必需的。

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

它只是归结为硬件在实现中的外观。在MIPS-V手册中,第68页有未引用的报价:

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

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


6
你经常要使用一些操作源值0值寄存器。在执行这些操作之前将寄存器清零会带来一些开销,因此,如果您可以仅使用提供的零而不是在需要时自己创建,就可以提高性能。示例包括添加进位标志。
JimmyB

3
在AVR架构上,gcc会在启动时将r1初始化为零,并且永远不会再触及该值,无论何时不能使用立即数0时,都将r1用作源。这里,出于性能原因,专用的零寄存器由编译器在软件中“模拟”。(大多数AVR具有32个寄存器,因此相对于可能的性能和代码大小优势,预留一个(实际上两个)寄存器不会花费太多。)
JimmyB

1
我不知道MIPS,但是将r0移到另一个寄存器可能比对那个寄存器进行XOR清除更快。
JimmyB

因此,您不同意零频率如此频繁以至于不值得在寄存器文件中定位吗?那么可能您是对的,因为这确实是有争议的,并且许多ISA选择不保留零寄存器。像当时其他有争议的功能一样,例如寄存器窗口,分支槽,“过去”的指令谓语...如果要设计ISA,则决定不使用它们就不必使用它们。
user3528438

2
阅读Berkeley RISC的旧论文之一《RISC I:精简指令集VLSI计算机》可能会很有趣。它显示了如何使用硬连线的零寄存器R0如何在单个RISC指令中实现多种VAX指令和寻址模式。
Mark Plotnick

Answers:


14

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位模式的零寄存器。以前没有零寄存器。(尽管该寄存器有点特殊,但在某些编码上下文中,它是零寄存器,在其他情况下,它则指定堆栈指针


我不认为MIPS使用标志,不是吗?零寄存器增加了无条件读取/写入某些地址的能力,而无需考虑任何CPU寄存器的内容,并有助于实现“立即移动”式的操作,但是其他移动可以通过对源本身进行逻辑或运算来完成。 。
超级猫

1
事实上,没有寄存器持有运算标记,而是有三个指令,帮助效仿通用的条件分支(sltsltisltu)。
Jarhmander

查看MIPS指令集,并根据我的理解,每条指令将在上一条指令执行时获取,我想知道是否会有一个操作码不会直接处理任何事情,而是说那很难如果执行立即模式指令,并且下一条提取的指令具有该位模式,则操作数的高16位将从预提取的指令中获取?那将使用两个字的两个周期指令来处理32位立即模式操作,而不必花费两个字和两个周期...
supercat

...先加载一个操作数,然后再加载第三个周期以实际使用它。
超级猫

7

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

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

考虑一个典型的现代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绝对是一个加分点。MIPS处理器的设计人员无需添加额外的RAZ寄存器,因为$ 0已经存在。由于RAZ可供汇编器使用,因此许多psuedo指令可用于MIPS,并且可以将其视为将解码逻辑的一部分推送到汇编器本身,而不是为每种指令类型创建专用格式以从软件中隐藏RAZ寄存器和其他架构一样 RAZ寄存器是一个好主意,这就是ARMv8复制它的原因。

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

由于MIPS最初是由一个小团队设计的,因此设计的简单性很重要,因此,按照RISC的精神明确选择$ 0。ARM32在体系结构级别保留了许多传统的CISC功能。


1
并非所有的ARM32 CPU都按照您描述的方式工作。对于更复杂的加载指令和/或写回寄存器,有些性能较低。因此,它们不能完全以相同的方式解码。
彼得·科德斯

6

Disclamer:我并不是很了解MIPS汇编程序,但是0值寄存器并不是该体系结构独有的,我想它的使用方式与我所知道的其他RISC体系结构相同。

对寄存器进行XOR运算以获得0将花费您一条指令,而使用预定义的0值寄存器则不会。

例如,mov RX, RY指令通常实现为add RX, RY, R0。如果没有0值寄存器,则xor RZ, RZ每次都必须使用mov

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


1
实施MOV Rx,Ryas 会有任何问题AND Rx,Ry,Ry吗?
超级猫

3
@supercat您将无法编码,mov RX, Imm或者mov RX, mem[RY]您的指令集仅支持单个立即数和每个指令单个内存访问。
德米特里·格里戈列耶夫

我不熟悉MIPS的寻址方式。我知道ARM具有[Rx + Ry <<标度]和[Rx + disp]模式,尽管能够将后者用于某些绝对地址可能在某些情况下很有用,但这通常不是必需的。可以使用零位移通过[Rx + disp]模拟直线[Rx]模式。MIPS使用什么?
超级猫

mov是一个不好的例子;您可以使用立即0而不是零寄存器来实现它。例如ori dst, src, 0。但是,是的,如果您没有mov-immediate,则需要一个操作码来注册addiu $dst, $zero, 1234,例如lui,用低16位而不是高16位来注册。并且您不能使用norsub构建单操作数而不是/ neg 。
彼得·科德斯

@supercat:万一您仍然想知道:经典MIPS仅具有一种寻址模式:register + disp16。现代MIPS为FP加载/存储添加了2寄存器寻址模式的其他操作码,从而加快了数组索引的速度。(但仍然不是整数加载/存储,可能是因为可能需要在整数寄存器文件中为2个地址寄存器+一个数据寄存器提供更多读取端口。请参阅 将寄存器用作偏移量
Peter Cordes,

3

将几根引线在寄存器组的末尾接地很便宜(比做成完整的寄存器便宜)。

进行实际的异或运算需要花一些时间和精力来切换门,然后将其存储在寄存器中,为什么要在现有的0值很容易获得的情况下付出这笔费用。

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


6
真正的代价R0不是将几条线接地,而是实际上必须在处理寄存器的每条指令中为其保留代码。
德米特里·格里戈里耶夫

异或是红色鲱鱼。xor-zeroing仅在x86上很好用,在x86上,CPU会识别惯用语并避免依赖输入。正如您所指出的,Sandybridge系列甚至没有为其运行uop,而只是在register-rename阶段对其进行处理。(在x86汇编中将寄存器设置为零的最佳方法是什么:xor,mov或and?)。但是在MIPS上,对寄存器进行XOR运算将具有错误的依赖关系。内存依存关系排序规则(HW等效于C ++ std::memory_order_consume)要求XOR传播依存关系。
彼得·科德斯

如果您没有零寄存器,则可以添加一个操作码以将立即数移到寄存器中。像lui但不左移16。因此您仍然可以使用一条指令将一个小数放入寄存器中。只允许零且具有错误的依赖关系将是疯狂的。(普通MIPS使用addiu $dst, $zero, 1234或创建非零值ori,因此您的“功耗”参数会失效。如果您希望避免触发ALU,则可以添加一个mov-immediate进行操作的操作码,而不用使用软件ADD或OR立即数为零。)
彼得·科德斯
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.