10
\$\begingroup\$

I think that I am looking for an answer to a trivia question. I am trying to understand why the MIPS architecture uses an explicit "zero" value in a register when you can achieve the same thing by just XOR'ing any register against itself. One could say that the operation is already done for you; however, I cannot really imagine a situation where you would be using a lot of "zero" values. I read Hennessey's original papers, and it just assigns a zero as a matter of fact without any real justification.

Does a logical reason to have a hard-coded binary assignment of zero exist?

update: In 8k of an executable from xc32-gcc for the MIPS core in the PIC32MZ, I have a single instance of "zero".

add     t3,t1,zero

the actual answer: I awarded the bounty to the person who had the information about MIPS and condition codes. The answer actually lies in the MIPS architecture for conditions. Although I was initially not wanting to assign time to this, I reviewed architecture for opensparc, MIPS-V, and OpenPOWER (this document was internal) and here are the summary findings. The R0 register necessary for comparison on branches due to the architecture of the pipeline.

  • integer compare against zero and branch (bgez,bgtz,blez,bltz)
  • integer compare two registers and branch (beq,bne)
  • integer compare two registers and trap (teq,tge,tlt,tne)
  • integer compare register and immediate and trap (teqi,tgei,tlti,tnei)

It just simply comes down to how the hardware looks in implementation. From the MIPS-V manual, there is an unreferenced quote on page 68:

The conditional branches were designed to include arithmetic comparison operations between two registers (as also done in PA-RISC and Xtensa ISA), rather than use condition codes (x86, ARM, SPARC, PowerPC), or to only compare one register against zero (Alpha, MIPS), or two registers only for equality (MIPS). This design was motivated by the observation that a combined compare-and-branch instruction ts into a regular pipeline, avoids additional condition code state or use of a temporary register, and reduces static code size and dynamic instruction fetch trac. Another point is that comparisons against zero require non-trivial circuit delay (especially after the move to static logic in advanced processes) and so are almost as expensive as arithmetic magnitude compares. Another advantage of a fused compare-and-branch instruction is that branches are observed earlier in the front-end instruction stream, and so can be predicted earlier. There is perhaps an advantage to a design with condition codes in the case where multiple branches can be taken based on the same condition codes, but we believe this case to be relatively rare.

The MIPS-V document does not hit at the author of the quoted section. I thank everyone for their time and consideration.

\$\endgroup\$
  • 6
    \$\begingroup\$ You often want to use a 0 valued register in some operation as source value. It would be some overhead to zero a register before those operations, so performance benefits if you can just use a provided zero instead of creating it yourself whenever one is needed. Examples include addition of a carry flag. \$\endgroup\$ – JimmyB Mar 21 '17 at 13:22
  • 3
    \$\begingroup\$ On the AVR architecture, gcc takes care to initialize r1 to zero upon startup and never touches that value again, using r1 as a source wherever an immediate 0 cannot be used. Here, the dedicated zero register is 'emulated' in software by the compiler for performance reasons. (Most AVRs have 32 registers, so setting one (two, actually) aside does not cost much in relation to the possible performance and code size benefits.) \$\endgroup\$ – JimmyB Mar 21 '17 at 13:29
  • 1
    \$\begingroup\$ I don't know about MIPS, but it may be faster to move r0 to another register compared to XORing that register to get it cleared. \$\endgroup\$ – JimmyB Mar 21 '17 at 13:32
  • \$\begingroup\$ So you disagree on the point that zero is so frequently that it worths a position in the register file? Then probably you are right because it's true this is controversial and there are a lot of ISAs choose not to reserve a zero register. Like other controversial feature at the time like register windows, branch slots, instruction predication from "the old days"...if you are to design an ISA, you don't have to use them if you decide not to. \$\endgroup\$ – user3528438 Mar 21 '17 at 15:11
  • 2
    \$\begingroup\$ It may be interesting to read one of the old Berkeley RISC papers, RISC I: A Reduced Instruction Set VLSI Computer. It shows how using a hard wired zero register, R0, allows a number of VAX instructions and addressing modes to be implemented in a single RISC instruction. \$\endgroup\$ – Mark Plotnick Mar 21 '17 at 19:31
12
+50
\$\begingroup\$

The zero-register on RISC CPUs is useful for two reasons:

It's a useful constant

Depending on restrictions of the ISA, you can't use a literal in some instructions encoding, but you can be sure you can use that r0 to get 0.

It can be used to synthesize other instructions

This is perhaps the most important point. As a ISA designer, you can trade-off a general purpose register to a zero-register to be able to synthesize other useful instructions. Synthesizing instructions is good because by having less actual instructions, you need less bits to encode an operation in a opcode, which frees-up space in the instruction encoding space. You can use that space to have e.g. bigger address offsets and/or literals.

The semantics of the zero-register is like /dev/zero on *nix systems: everything written to it is discarded, and you always read back 0.

Let's see a few examples of how we can make pseudo-instructions with the help of the r0 zero-register:

; ### 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]

The case of MIPS

I looked more closely at the MIPS instruction set. There are a handful of pseudo-instructions that uses $zero; they are mainly used for branches. Here are some examples of what I've found:

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

As for why you have found only one instance of the $zero register in your disassembly, perhaps it's your disassembler that is smart enough to transform known sequences of instructions into their equivalent pseudo-instruction.

Is the zero-register really useful?

Well, apparently, ARM finds having a zero-register useful enough that in their (somewhat) new ARMv8-A core, which implement AArch64, there's now a zero-register in 64-bit mode; there wasn't a zero-register before. (The register is a bit special though, in some encoding contexts it's a zero-register, in others it instead designates the stack pointer)

\$\endgroup\$
  • \$\begingroup\$ I don't think MIPS uses flags, does it? The zero register adds the ability to unconditionally read/write ccess certain addresses without regard for the content of any CPU registers and helps facilitate a "mov immediate"-style operation, but other movs could be done by logical or-ing the source with itself. \$\endgroup\$ – supercat Mar 23 '17 at 21:55
  • 1
    \$\begingroup\$ Indeed, there is no register that hold arithmetic flags, instead there are three instructions that help emulate common conditional branches (slt, slti, sltu). \$\endgroup\$ – Jarhmander Mar 24 '17 at 4:49
  • \$\begingroup\$ Looking at the MIPS instruction set, and given that from what I understand each instruction will be fetched by the time the previous instruction executes, I wonder if it would have been difficult to have an opcode which doesn't to anything directly but instead say that if an immediate-mode instruction executes and the next fetched instruction has that bit pattern, the upper 16 bits of the operand will be taken from the prefetched instruction? That would 32-bit immediate-mode operations to be handled with a two-word two-cycle instruction rather than having to spend two words and two cycles... \$\endgroup\$ – supercat Mar 24 '17 at 14:30
  • \$\begingroup\$ ...loading an operand and then a third cycle to actually use it. \$\endgroup\$ – supercat Mar 24 '17 at 14:31
7
\$\begingroup\$

Most ARM/POWER/SPARC implementations have a hidden RAZ register

You might think that ARM32, SPARC etc do not have a 0 register but in fact they do! At the micro-architecture level, most CPU design engineers add in a 0 register that may be invisible to software (ARM's zero register is invisible) and use that zero register to streamline instruction decode.

Consider a typical modern ARM32 design that has a software invisible register, say R16 wired to 0. Consider the ARM32 load, many cases of ARM32 load instruction fall into one of these forms (Ignore pre-post indexing for a while to keep the discussion simple)...

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

Inside the processor, this decodes to a general

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

before entering the issue stage where registers are read. Note that rx represents the register to write-back the updated address. Here are some decode examples:

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.

At the circuit level, all three loads are actually the same internal instruction and an easy way to get this kind of orthogonality is to create a ground register R16. Since R16 is always grounded, these instructions naturally decode correctly without any extra logic. Mapping a class of instructions to a single internal format greatly helps in superscalar implementations as it reduces logic complexity.

Another reason is a streamlined way to throw away writes. Instructions may be disabled by simply setting the destination register and flags to R16. There is no need of creating any other control signal to disable the write-back etc.

Most processor implementations irrespective of architecture end up with a RAZ register model early on in the pipeline. The MIPS pipeline essentially starts at a point that would in other architectures be a few stages in.

MIPS made the right choice

Thus, a read-as-zero register is almost mandatory in any modern processor implementation and MIPS making it visible to software is definitely a plus point given how it streamlines the internal decode logic. Designers of MIPS processors need not add in an extra RAZ register since $0 is already at ground. Since RAZ is available to the assembler, a lot of psuedo instructions are available to MIPS and one can think of this as pushing part of the decode logic to the assembler itself instead of creating dedicated formats for each instruction type to hide the RAZ register from software as with other architectures. The RAZ register is a good idea and that's why ARMv8 copied it.

If ARM32 had a $0 register, the decode logic would have become simpler and the architecture would have been much better in terms of speed, area and power. For example, of the three versions of LDR presented above, only 2 formats would be needed. Similarly, there is no need to reserve decode logic for the MOV and MVN instructions. Also, CMP/CMN/TST/TEQ would become redundant. There would also be no need to differentiate between short (MUL) and long multiplication (UMULL/SMULL) since short multiplication could be considered as long multiplication with the high register set to $0 etc.

Since MIPS was initially designed by a small team, the simplicity of design was important and thus $0 was explicitly chosen in the spirit of RISC. ARM32 retains lots of traditional CISC features at the architectural level.

\$\endgroup\$
  • \$\begingroup\$ Not all ARM32 CPUs work the way you describe. Some have lower performance for more complex load instructions and/or for write-back to the register. So they can't all decode exactly the same way. \$\endgroup\$ – Peter Cordes May 13 at 23:37
6
\$\begingroup\$

Disclamer: I don't really know MIPS assembler, but 0-value register is not unique to this architecture, and I guess it is used in the same way as in other RISC architectures I know.

XORing a register to obtain 0 will cost you one instruction, while using a predefined 0-value register will not.

For example, mov RX, RY instruction is often implemented as add RX, RY, R0. Without a 0-value register, you'd have to xor RZ, RZ every time you want to use mov.

Another example is cmp instruction and its variants (like "compare and jump", "compare and move", etc), where cmp RX, R0 is used to test for negative numbers.

\$\endgroup\$
  • 1
    \$\begingroup\$ Would there be any problems implementing MOV Rx,Ry as AND Rx,Ry,Ry? \$\endgroup\$ – supercat Mar 21 '17 at 14:22
  • 3
    \$\begingroup\$ @supercat You won't be able to encode mov RX, Immor mov RX, mem[RY] if your instruction set only support a single immediate value and a single memory access per instruction. \$\endgroup\$ – Dmitry Grigoryev Mar 21 '17 at 17:36
  • \$\begingroup\$ I'm not familiar with what addressing modes the MIPS has. I know the ARM has [Rx+Ry << scale] and [Rx+disp] modes, and while being able to use the latter for some absolute addresses could be useful in some cases it's generally not essential. A straight [Rx] mode could be emulated via [Rx+disp] using zero displacement. What does the MIPS use? \$\endgroup\$ – supercat Mar 21 '17 at 18:57
  • \$\begingroup\$ mov is a poor example; you could implement it with an immediate 0 instead of a zero register. e.g. ori dst, src, 0. But yes, you'd need an opcode for mov-immediate to register if you didn't have addiu $dst, $zero, 1234, like lui but for the lower 16 bits instead of the upper 16. And you couldn't use nor or sub to build one-operand not / neg. \$\endgroup\$ – Peter Cordes May 13 at 23:30
  • \$\begingroup\$ @supercat: in case you're still wonder: classic MIPS only has a single addressing mode: register + disp16. Modern MIPS added other opcodes for 2-register addressing modes for FP loads/stores, speeding up array indexing. (But still not for integer load/store, maybe because that could require more read ports in the integer register file for 2 address regs + a data reg for a store. See Using a register as an offset) \$\endgroup\$ – Peter Cordes May 13 at 23:34
3
\$\begingroup\$

Tying a few leads to ground at the end of your register bank is cheap (cheaper than making it a full blown register).

Doing the actual xor takes a bit of power and time to switch the gates and to then store it in the register, why pay that cost when an existing 0 value can easily be available.

Modern cpus also have a (hidden) 0-value register they can use as the result of a xor eax eax instruction through register renaming.

\$\endgroup\$
  • 5
    \$\begingroup\$ The real cost of R0 is not in grounding a few wires but in the fact that you have to reserve a code for it in every instruction which deals with registers. \$\endgroup\$ – Dmitry Grigoryev Mar 21 '17 at 17:30
  • \$\begingroup\$ The xor is a red herring. xor-zeroing is only good on x86, where CPUs recognize the idiom and avoid a dependency on the inputs. As you point out, Sandybridge-family doesn't even run a uop for it, just handling it in the register-rename stage. (What is the best way to set a register to zero in x86 assembly: xor, mov or and?). But on MIPS, XORing a register would have a false dependency; memory dependency ordering rules (HW equivalent of C++ std::memory_order_consume) require XOR to propagate the dependency. \$\endgroup\$ – Peter Cordes May 13 at 23:41
  • \$\begingroup\$ If you didn't have a zero register, you'd include an opcode to move an immediate to a register. Like lui but not left-shifted by 16. So you can still put a small number in a register with one instruction. Allowing only zero with a false dependency would be insane. (Normal MIPS creates non-zero values with addiu $dst, $zero, 1234 or ori, so your "power cost" argument breaks down. If you wanted to avoid firing up an ALU, you'd include an opcode for mov-immediate to register instead of having software ADD or OR an immediate with zero.) \$\endgroup\$ – Peter Cordes May 13 at 23:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.