Is it legal to have same register as operands in one instruction?

Viewed 382

For example, below is a X86-64 instruction:

movq (%rdi),%rdi

It reads the content (pointed by %rdi) and set this content to %rdi, which is valid in sequential logic?

Or do we have to assign it to a different register and move it back as:

movq (%rdi), %rsi
movq rsi, %rdi
2 Answers

It is not only legal but very common to repurpose a register in the same instruction.

In machine code, the CPU registers are fast and thus preferred storage, but they are limited in number.  Whereas in high level languages, you can't run out of variables (declare a new one whenever you need it) — compilers and assembly language programmers tend to map variables that don't overlap in usage to the same storage.

Further, in machine code, we see many short-lived variables that we don't see in high level languages — these are called temps or expression temporaries.  A simple expression like a[i] in machine code requires computing a + i * 4, which may require a temp for holding a or i or maybe i * 4, etc..  These temps are short lived variables and provided they are no longer needed in subsequent expressions, then the CPU registers holding them can be immediately repurposed for something else.

Some ISAs have restrictions for some instructions, like older ARM has some limitations on using the same reg for a source and destination for umull. These hazards are always documented in the instruction-set reference manual1.

Intel and AMD's x86 manuals don't document any such limitation (except for push sp on 8086 / 1862), so you can always assume that an x86-64 instruction reads all its input operands before writing any output operands, exactly like you'd expect. (That's the normal state of affairs for the internal pipeline in CPUs anyway.)


Footnote 1: for example, Keil's ISA reference for UMULL{S}{cond} RdLo, RdHi, Rn, Rm says:
Rn must be different from RdLo and RdHi in architectures before ARMv6.
(The instruction does a full-multiply, like x86 mul, but with an explicit pair of outputs instead of EDX:EAX, and with 2 explicit sources.)

Weird restrictions, if any exist for an ISA, usually involve multiply; for example MIPS also plenty of weirdness around multu and its HI:LO register pair. This old MIPS R3000 manual explains that a multu right after an mflo could still start even if an interrupt happened, resulting in corrupt state after resuming from an interrupt. You need to separate reading one multiply result back into normal regs by 2 instructions before starting another multiply. See also Raymond Chen's blog.

These software "hazards" are the same kind of concept as https://en.wikipedia.org/wiki/Hazard_(computer_architecture), but they're cases the hardware doesn't handle, rather than cases that out-of-order exec needs to detect.

x86 doesn't have any of these software hazards; I only mention other ISAs for comparison and as a point of interest.

Footnote 2:

Early x86 CPUs were microcoded, not pipelined, and the microcode implementation of push actually exposed a weird effect on the earliest x86 CPUs. See the pre-286 note in Intel's manual entry for push:

For IA-32 processors from the Intel 286 on, the PUSH ESP instruction pushes the value of the ESP register as it existed before the instruction was executed. (This is also true for Intel 64 architecture, real-address and virtual-8086 modes of IA-32 architecture.) For the Intel® 8086 processor, the PUSH SP instruction pushes the new value of the SP register (that is the value after it has been decremented by 2).

But for 286 and later (including all x86-64 CPUs), it works as if all operands are read before any are written, including for push rsp or push [rsp + 8].

The PUSH ESP instruction pushes the value of the ESP register as it existed before the instruction was executed. If a PUSH instruction uses a memory operand in which the ESP register is used for computing the operand address, the address of the operand is computed before the ESP register is decremented.

pop also has to document the fact that pop rsp does the stack pointer decrement first, before writing the load result into the register. So pop rsp = mov rsp, [rsp], without add rsp, 8 after. (This is described in the text. Intel's pseudocode doesn't use a temporary, so it doesn't correctly describe that special case.)


And BTW, you can find lots of examples of instructions like that in compiler output. e.g. when the source is a memory operand with a register addressing mode, you're pointer-chasing through a linked list or tree (if this is in a loop, otherwise you're just reusing a register as you wade through levels of indirection).

int foo(int ****p) {
   int tmp = ****p;
   return tmp * 2;
}

Compiled by clang 10.1 -O3 for x86-64:

foo(int****):
        mov     rax, qword ptr [rdi]
        mov     rax, qword ptr [rax]
        mov     rax, qword ptr [rax]         # write-only destination
        mov     eax, dword ptr [rax]
        add     eax, eax                     # EAX += EAX,  read-write destination
        ret
Related