[HN Gopher] The Art of Picking Intel Registers (2003)
___________________________________________________________________
The Art of Picking Intel Registers (2003)
Author : Tomte
Score : 68 points
Date : 2021-02-07 15:39 UTC (7 hours ago)
(HTM) web link (www.swansontec.com)
(TXT) w3m dump (www.swansontec.com)
| blueflow wrote:
| The opcode encoding goes AX CX DX BX, not AX BX CX DX. This is a
| bit like the XKCD comic about bad kerning - you need to point it
| out and then people cant be unbothered by it.
| kiwidrew wrote:
| That's because Intel made the 8086 kind-of-sort-of backwards
| compatible with the 8080; it had six 8-bit registers (A Flags B
| C D E) plus a 16-bit register pair (H L), in that order.
|
| 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.)
| dragontamer wrote:
| Register renaming on modern x86 (skylake or zen) doesn't even use
| an execution unit. It may not even use a spot in the uOP cache
| IIRC. That means 'mov eax, ebx' uses minimal resources.
|
| 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.
| rmhsilva wrote:
| Ahh this was a great great article back in the day. Lots of fun
| on smashthestack.
|
| See also:
|
| - Standard x86 calling conventions --
| http://unixwiz.net/techtips/win32-callconv-asm.html
|
| - A x86 instruction reference -- http://ref.x86asm.net/
|
| EDIT: formatting
| gbrown_ wrote:
| Another interesting point is ROP gadgets, which OpenBSD tries to
| reduce by preferring to avoid EBX/RBX registers [1]. This and
| more on the subject can be found in this paper [2].
|
| [1] https://marc.info/?l=openbsd-tech&m=150869222214001&w=2
|
| [2] https://www.openbsd.org/papers/asiabsdcon2019-rop-paper.pdf
| saagarjha wrote:
| (Of course, it is dubious as to whether this actually helps
| much.)
| fctorial wrote:
| The author claims that using these patterns will result in faster
| code. Has anyone done the actual benchmarks and tested this
| claim?
| dang wrote:
| If curious see also
|
| 2018 https://news.ycombinator.com/item?id=16896536
|
| 2013 https://news.ycombinator.com/item?id=5424649
| chrisseaton wrote:
| > Using registers according to Intel's original plan allows the
| code to take full advantage of these optimizations.
| Unfortunately, this seems to be a lost art. Few coders are aware
| of Intel's overall design, and most compilers are too the
| simplistic or focused on execution speed to use the registers
| properly.
|
| 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.
| ajross wrote:
| Well... it's complicated.
|
| 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.
| CalChris wrote:
| Amplifying your point, mops are grouped into Ways and Ways
| are 6 mops on Intel and 8 mops on AMD. Ways have constraints;
| violating a constraint will necessitate a spill.
| stage bypass macro + micro fusion mops mop
| 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.
| fctorial wrote:
| Is this documented somewhere? Where do I look if I want
| more information about uops?
| vlovich123 wrote:
| I've never seen this detailed high level breakdown before
| so super helpful, thanks!
|
| 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?)
| jcranmer wrote:
| Extra pages use more TLB resources is I think the message
| GP is trying to convey here.
| h2odragon wrote:
| I'd go so far as to say _extreme_ size optimization, as indeed
| the author is doing... Demoscene tricks and shellcode are about
| the only places that matters now, i think. Stunts.
|
| 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.
| mhh__ wrote:
| Generating small code can be desirable - the uOP$ is a _lot_
| smaller than the I$
|
| 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
| tom_mellior wrote:
| Could you explain how one would get a SIMD monster for a
| factorial function?
| mhh__ wrote:
| nwallin has provided an example.
|
| 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.
| nwallin wrote:
| https://godbolt.org/z/61xGW7
|
| 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;
| dragontamer wrote:
| uOPs are proprietary and undocumented: potentially changing
| between architectures even from the same company. So it's
| hard to generalize.
|
| 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...
| mhh__ wrote:
| We know - and your compiler knows - exactly how many uOps
| are generated and which ports they execute on them - just
| not what they really do. Intel's performance counters can
| be fairly elucidating as to what your computer is doing -
| but - finding one you actually want can often mean
| scrolling through a list of thousands.
|
| My point is that small code is useful - unrolling a twice
| nested loop can kill performance if you aren't careful.
| zowanet wrote:
| > Note carefully that the context here is size optimisation
| only.
|
| 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.
|
| [0]: https://en.wikipedia.org/wiki/Register_renaming
| saagarjha wrote:
| Yeah, this seems mainly aimed at demo programmers, where the
| absolute smallest code size is the concern rather than the best
| performance.
| jeffbee wrote:
| This knowledge is not obsolete. REP STOS/MOVS is the right way
| to move memory now, and its operands are SI, DI, and CX. It is
| still useful to know what the registers are supposed to mean.
| dragontamer wrote:
| From my understanding, looping avx512 or avx2 mov might be
| faster than rep stos in many cases.
| jeffbee wrote:
| The number of such cases is decreasing, though. Recent (ICL
| and "later" whatever that means) Intel cores have fast-
| short-rep-movsb which reduces the startup time of the REP.
| Considering the icache pressure of a hyperoptimized memcpy,
| it is probably the right way to go for new code.
|
| 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"
| dragontamer wrote:
| Good to hear.
|
| Rep movs is the obvious way to represent memmove and
| memcpy. Avx moves should have never been faster than that
| sequence.
| chrisseaton wrote:
| Don't Intel now recommend 'implementing memcpy using Enhanced
| REP MOVSB and STOSB might not reach the same level of
| throughput as using 256-bit or 128-bit AVX alternatives'?
| saagarjha wrote:
| For implementing an optimized memcpy, I believe that is
| still true (although the gap between rep movsb and a
| nontemporal copy using vector registers is not all that
| large). But for many cases the code size improvement (a
| couple of bytes versus hundreds for a typical vectorized
| implementation) or restrictions on register use (kernel)
| mean that it can be a good choice.
| toast0 wrote:
| All things being equal, smaller code is better[1]. Of course,
| if things are not equal, and loop is slower than building it
| yourself, then build it yourself except where you're size
| constrained.
|
| 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.
| chrisseaton wrote:
| When Intel themselves don't recommend using the instruction
| in their optimisation manual... I think you can take that as
| a good indicator that it isn't going to be fast even before
| you start benchmarking.
| mhh__ wrote:
| Cachelines are 64 bytes on most architectures these days.
|
| Zen 3(?) has some other requirements along these lines but I
| don't remember what exactly.
| 38491239821398 wrote:
| Interestingly, the M1 (and probably other Apple Silicon
| CPUs) is an exception: % sysctl
| hw.cachelinesize hw.cachelinesize: 128
| jeffbee wrote:
| Hyperoptimizing for code size can be counterproductive. If
| you pack several branches or branch targets into the same
| cache line, the CPU might lose its ability to predict them.
| Making your code bigger (inserting NOP to align branch
| targets, for example) will often be worth the tradeoff in
| pure size.
___________________________________________________________________
(page generated 2021-02-07 23:01 UTC)