[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)