The Art of Picking Intel Registers (2003)
swansontec.com
swansontec.com
Note carefully that the context here is size optimisation only. When they talk about 'optimised' they just mean smaller code.
The advice is pre-historic and counter-productive for all other purposes. Compilers don't generate as is being advocated here, not because they're ignorant of this 'lost art', but because in modern implementations of the architecture there's no point! In fact I think some of the old instructions recommended here like 'loop' are almost always slower than simpler modern equivalents using any registers and multiple instructions.
Makes it no less cool. I needed some of these tricks once, and have had many years of joy since in not needing them again.
For example, compilers aren't very good at spotting overflows, so you can end up with a 150 instruction SIMD monster for the factorial function when pentium pro code could've been faster for the ints that matter.
All this x86 weirdness is partly why the schedulers compilers actually use are quite detached from the textbooks - GCC builds a FSM to model the decoder for example, as that's where a lot of the bottleneck will be for a CISC ISA like X86
But it's my understanding that uOPs are constant sized. Having a variable length uOP seems counterproductive. If something is so complex that it won't fit in one uOP, then it's probably preferable to encode it as two separate uOPs.
We can get an idea of what is multiple uOPs by looking at the pipelines and latency charts. But even then: multiplication is likely one uOP despite taking multiple clock ticks of latency. So it's not exactly a precise science...
My point is that small code is useful - unrolling a twice nested loop can kill performance if you aren't careful.
IIRC uops can take more than one solt in the uop cache if they need to encode large constants, so they are technically variable length.
Basically, the compiler isn't smart enough to realize that the loop in the factorial function couldn't possibly execute more than a few iterations before returning. Basically, it tries to do this:
int a = 1;
int b = 2;
int c = 3;
int d = 4;
for (int i = 5; i < x; i += 4) {
a *= i;
b *= i + 1;
c *= i + 2;
d *= i + 3;
}// handwave a bunch of crap about edge cases
return a * b * c * d;
I think I've lost the results now, but when benchmarked it's basically neck and neck between size and speed but similar speed for less code is almost always a good thing in this case.
Indeed, and even if other benefits were once possible in the early x86 era, modern CPUs use register renaming[0] which presumably would make such benefits redundant.
If you're interested see Google's paper, AsmDB: Understanding and Mitigating Front-End Stalls, section 4.4 Memcmp and the perils of micro-optimization, in which they say that REP CMPS beats glibc memcmp in full-scale benchmarks. "On large-footprint workloads like websearch, it reduced cycles in memcmp more than twofold and showed an overall 0.5%-1% end-toend performance improvement." And that was on Haswell, before people started selling parts with "fast short cmpsb"
Rep movs is the obvious way to represent memmove and memcpy. Avx moves should have never been faster than that sequence.
For smaller sizes, rep stos can be as much as 20x slower than the alternative.
- intel xeon e3-1246
- intel core i3-5005u
- amd ryzen 3960X
- amd epyc 7402p
Comparing my own[0] memset implementation, glibc's, and bionic's.
I think that benchmarking memset by itself is folly, because you may end up with a very large function that is fast on its own, but which trashes your icache when run in a real application. That is the conclusion of the paper I linked elsewhere in this thread: that rep cmpsb, even on old CPUs where it is slow, beats glibc memcmp in real full-scale applications because glibc memcmp is 6KB long and has a tendency to evict dozens of lines of icache. memset, memcpy, and memmove have the same hazard.
That being said, all the implementations of memset I tested except for glibc's are in the neighborhood of 100-300 bytes (also including the freebsd version, which doesn't use any simd), which is a fair sight smaller than 6k.
Also, you really have to benchmark these things to know for sure, and it can change between processor generations and the author couldn't have tested Zen 3 in 2003; and we'll have a hard time testing Pentium M in 2021.
[1] Memory bandwidth is limited, cache sizes are limited, even though both are huge. Otoh, the quantum of useful savings is a cache line, 64 bytes on modern amd64 if I'm reading correctly. If you save some bytes, but not a cache line, it probably doesn't help anything. If you only save one cacheline, the difference will likely be hard to measure, but could be measurable depending on circumstances.
Zen 3(?) has some other requirements along these lines but I don't remember what exactly.
% sysctl hw.cachelinesize
hw.cachelinesize: 128https://www.mono-project.com/news/2016/09/12/arm64-icache/
Perhaps, either Apple is fooling the userspace software or is using 128b cachelines for their little cores too.
hw.cachelinesize: 128
hw.l1icachesize: 131072
hw.l1dcachesize: 65536
The reported L1I and L1D matches up with the little cores.Also very interesting: if you run sysctl under Rosetta, it reports 64 bytes:
% arch -x86_64 /bin/bash -c "sysctl hw.cachelinesize"
hw.cachelinesize: 64
This information is exposed specifically so that app developers can optimize for the cache line size in use. I very much doubt Apple would report a false number.As for Samsung: my guess is that Apple has been using 128-byte cache lines for a long time, and Samsung simply copied that because Apple did it, without understanding the implications. That would be totally in line with what I expect from them.
No, this number is used for compatibility reasons. OS is free to pull shenanigan behind apps to take care of CPU cache line size changing underneath them (like using a larger size so it'll align on everything; 128b aligned is 64b aligned too).
I'm sure there are additional, more advanced techniques, but I also have a hard time believing they wouldn't use the lower bits of the address.
First, "size only" optimization isn't really a thing. The instruction cache is a finite resource, so using less of it allows more code to fit, which increases performance (this is less true in the post-SNB world where the first level of instruction caching is the uOp cache and not an image of memory).
Also, there do remain special purpose registers which are involved in compiler-generated code. Multiplication still clobbers RAX and RDX for example, so if you have hand-generated assembly that wants to touch those it will force nearby compiler-generated multiplies to issue extra instructions to arrange things. Likewise RSI/RDI are still the preferred loop counters for memcpy on most toolchains, so if you're mucking with them needlessly (e.g. where Rnn registers would do) you're making more work for the compiler.
It's true that lots of hardware work over time has acted to normalize the instruction set, so none of these optimizations are particularly large. But the details are still real and worth knowing.
stage bypass
macro + micro fusion
μops
μop Way
functional unit
Loop Stream Detector
16B decoder fetch line
64B instruction cache line
instruction cache
page
DRAM
Picking the right register might occasionally help with a decoder fetch line here and there, and progressively less often with the following instruction packing scenarios. But the first 6 are more important for performance although Intel spends a lot of transistors and dollars to make that less of an issue. They also have nothing (IIRC) with register names.What’s the difference between page and DRAM? In fact, I probably can only guess at most of these so if you could provide definitions that would be good (or are there Wikipedia pages on all these?)
Of course, there's Agner Fog's Microarchitecture and the Intel® 64 and IA-32 Architectures Optimization Reference Manual.
There are about 11 people who really understand this stuff and they all know each other, and we all read them. Travis Downs and Geoff Langdale (Hyperscan) come to mind.
[1] easyperf.net
[1] https://marc.info/?l=openbsd-tech&m=150869222214001&w=2
[2] https://www.openbsd.org/papers/asiabsdcon2019-rop-paper.pdf
Unsurprising, since the paper seems to be written by someone with no actual knowledge of x86 in the first place. Eg, in Alternate Code Generation, they recommend rewriting:
48 89 C3 mov rbx, rax
# as
48 87 D8 xchg rax, rbx
48 89 D8 mov rax, rbx
48 87 D8 xchg rax, rbx
# when they should simply be writing
48 8B D8 mov rbx, rax $ note that this is *literally the same instruction* at the assembly level
And even when the xchg's are actually needed (eg for mod/rm /0 and /1 opcode groups), they should be encoded properly: 48 C1 C3 04 rol rbx, 4
# to
48 93 xchg rax, rbx # we have dedicated instructions for this for a reason
48 C1 C0 04 rol rax, byte 42
48 93 xchg rax, rbx # I think we *do* need the 48 on these, unfortunately; blame AMD
The former (that is, the policy of assembling register-to-register mod/rm instructions between a ax/cx and a bx/dx with the former in the effective address (not "destination") slot) eliminates upwards of three quarters of mod/rm-ret gadgets (including in hand-coded inline assembly!) at zero cost, and should have been the first option, before even thinking about register allocation.See also:
- Standard x86 calling conventions -- http://unixwiz.net/techtips/win32-callconv-asm.html
- A x86 instruction reference -- http://ref.x86asm.net/
EDIT: formatting
It still matters for taking up L1 space, but today's processors probably don't care about this list art anymore outside of code size.
Years ago, when mov actually created a write hazard and caused bubbles in your pipeline, this stuff was important. But not nearly as important anymore.
This post is still useful as a historical note, and as a reminder for how low level details can dramatically change over the decades, even within the same x86 instruction set.
No that's the opposite of what the author claims. They claim size, compressibility, and readability.
> The goal is to put as much high-quality music, graphics, and animation as possible into only 4096 bytes.
AL corresponds to the 8080's A register and AH to the flags (which is also why the LAHF and SAHF instructions exist). CX corresponds to B+C and DX corresponds to D+E. Finally, BX is the 8086 equivalent of the 8080's pointer register HL.
(The 8086 didn't use the same opcodes as the 8080 but it was designed this way so that a simple translator could 'convert' existing 8080 code.)
This is like a urban legend that i have never found the source for. I stopped believing it after learning that the 8080 encoding order is BCDEHLMA, with the accumulator being last at position 7.
...and detecting 11 or 00 is easier than 01 or 10 in hardware, because it saves an inversion and thus a few transistors.