The Surprising Subtleties of Zeroing a Register (2013)
randomascii.wordpress.com
randomascii.wordpress.com
An x86-64 CPU has sixteen integer registers, but 100-200 physical integer registers. Every time an instruction writes to, say, RAX the renamer chooses an available physical register and does the write to it, recording the fact that RAX is now physical-register #137. This allows the breaking of dependency chains, thus allowing execution parallelism.
In the case of the zeroing instructions the register renamer says "RAX now maps to physical-register #0" where that is a register reserved for the purpose of being zero. No execution is needed. It's beautiful.
For more details read up on out-of-order execution and register renaming.
Butler Lampson mentions register renaming as one of a dozen paradigmatic neat techniques in this 2015 presentation:
http://bwlampson.site/Slides/Hints%20and%20principles%20(HLF... Edit: corrected link
It's easy for software people not to ever encounter it!
Yep, because it does its magic without any intervention required.
One concrete example that I like is this:
mov rax,[rsi+0] mov [rdi+0],rax mov rax,[rsi+8] mov [rdi+8],rax
This moves sixteen bytes of memory, all of it going through rax. Without register renaming the third instruction can't run until the second instruction completes, because they're both using rax. With register renaming the third instruction uses a different physical register for rax, and the out-of-order engine can do the loads in parallel and the stores in parallel.
Before anybody freaks out - register renaming doesn't change the semantics of your program. It just makes your program run faster. It just means that you can write code like the above - there's no need to rearrange the instructions and use multiple temporary registers, because the CPU will do it for you, and will dirty fewer architectural registers.
x, y = y, x
Just becomes register renaming? What about array[i], array[j] = array[j], array[i]
?But everything that golang (or C or C++ or whatever) does has to be expressed in assembly language, and assembly language doesn't directly control register renaming - it's a hidden implementation detail.
The register rename stage, which does the zeroing, can handle four instructions per cycle. It’s not immediately obvious from the picture, but after instructions are fetched from the uop cache, they are sent to a 28-entry decoder queue, which can act as a small loop buffer. From there, four uops per cycle are sent to the allocate/rename stage (which is part of the ROB block in the first picture).
Earlier, the article says that the process can perform four instructions per cycle. Just to confirm, this isn't normally referred to as taking "0.25 cycles per instruction", right?
is this down to pipelining, or is that separate?
On Skylake, for instance, the execution units are:
Port 0: int ALU, FP ALU, vector ALU, integer multiply/divide, branch
Port 1: int ALU, FP ALU, vector ALU
Port 2: Load unit
Port 3: Load unit
Port 4: Store unit
Port 5: integer ALU, vector ALU
Port 6: integer ALU, branch
Port 7: Store unit [address component]
Every instruction (or, more accurately, every micro-op; instructions don't exist at this point) can get scheduled onto one of these ports if that port has the capability to process it.
Of course, there's also a limitation on the abilities of the instruction decode unit on how many instructions it can decode and convert into micro-ops each cycle.
Pipelining means that instruction fetch, decode, execution, and retirement are done in separate stages, each one taking at least one cycle.
Super-scalar/parallelism means that there are multiple units for all of these stages so the CPU can fetch many instructions in parallel, decode many instructions in parallel, etc.
The cool/critical thing here is that many Intel CPUs can execute three XOR instructions in parallel but can retire four. Because the CPU recognizes XOR of an instruction with itself as a special case it skips the execution stage and can then process four per cycle.
Or, more likely in real code, the execution units are available for other instructions to use.
For example, the latency of a multiply instruction is usually about 3-4 cycles. But the reciprocal throughput is generally 1 (or 0.5 if you have two multiply units)--the execution unit is pipelined to allow a new input every cycle.
The article was written in 2012, but was last updated in 2013, so dealer's choice?