title: How exactly do partial registers on Haswell/Skylake perform? Writing AL seems to have a false dependency on RAX, and AH is inconsistent
body: <p>This loop runs at one iteration per 3 cycles on Intel Conroe/Merom, bottlenecked on <code>imul</code> throughput as expected.  But on Haswell/Skylake, it runs at one iteration per 11 cycles, apparently because <code>setnz al</code> has a dependency on the last <code>imul</code>.</p>
  
  <pre><code>; synthetic micro-benchmark to test partial-register renaming
      mov     ecx, 1000000000
  .loop:                 ; do{
      imul    eax, eax     ; a dep chain with high latency but also high throughput
      imul    eax, eax
      imul    eax, eax
  
      dec     ecx          ; set ZF, independent of old ZF.  (Use sub ecx,1 on Silvermont/KNL or P4)
      setnz   al           ; ****** Does this depend on RAX as well as ZF?
      movzx   eax, al
      jnz  .loop         ; }while(ecx);
  </code></pre>
  
  <p>If <code>setnz al</code> depends on <code>rax</code>, the 3ximul/setcc/movzx sequence forms a loop-carried dependency chain.  If not, each <code>setcc</code>/<code>movzx</code>/3x<code>imul</code> chain is independent, forked off from the <code>dec</code> that updates the loop counter.  The 11c per iteration measured on HSW/SKL is perfectly explained by a latency bottleneck: 3x3c(imul) + 1c(read-modify-write by setcc) + 1c(movzx within the same register).</p>
  
  <hr>
  
  <p><strong>Off topic: avoiding these (intentional) bottlenecks</strong></p>
  
  <p>I was going for understandable / predictable behaviour to isolate partial-reg stuff, not optimal performance.</p>
  
  <p>For example, <code>xor</code>-zero / set-flags / <code>setcc</code> is better anyway (in this case, <code>xor eax,eax</code> / <code>dec ecx</code> / <code>setnz al</code>).  That breaks the dep on eax on all CPUs (except early P6-family like PII and PIII), still avoids partial-register merging penalties, and saves 1c of <code>movzx</code> latency.  It also uses one fewer ALU uop on CPUs that <a href="https://stackoverflow.com/questions/33666617/what-is-the-best-way-to-set-a-register-to-zero-in-x86-assembly-xor-mov-or-and/33668295#33668295">handle xor-zeroing in the register-rename stage</a>.  See that link for more about using xor-zeroing with <code>setcc</code>.</p>
  
  <p>Note that AMD, Intel Silvermont/KNL, and P4, don't do partial-register renaming at all.  It's only a feature in Intel P6-family CPUs and its descendant, Intel Sandybridge-family, but seems to be getting phased out.</p>
  
  <p>gcc unfortunately does tend to use <code>cmp</code> / <code>setcc al</code> / <code>movzx eax,al</code> where it could have used <code>xor</code> instead of <code>movzx</code> <a href="https://gcc.godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(j:1,source:&#39;//+gcc+uses+xor/cmp/setcc+in+a+simple+case%0Aint+boolean(int+a)+%7B+return+a!!%3D5%3B+%7D%0A%0A%0A//+When+things+get+complex+enough,+gcc+uses+setcc/movzx.%0A//+But+fortunately+into+a+register+that+was+already+part+of+the+same+dep+chain%0A//+clang+uses+R8%0Aunsigned+int+countmatch(unsigned+int+arr%5B%5D,+unsigned+int+key)%0A%7B%0A++++unsigned+count+%3D+0%3B%0A++++for+(int+i%3D0%3B+i%3C1024+%3B+i%2B%2B)+%7B%0A++++++++//+changing+%60key%60+to+%607%60+doesn!&#39;t+get+gcc+to+use+xor/cmp/setcc:+It!&#39;s+not+trying+to+avoid+REX+prefixes,+maybe+just+some+register-pressure+threshold%0A++++++++count+%2B%3D+((arr%5Bi%5D+%26+key)+!!%3D+5)%3B%0A++++%7D%0A++++return+count%3B%0A%7D%0A%0A//+we+can+even+convince+gcc+and+clang+to+do+%0A//+%22dangerous%22+things+with+partial+registers%0A//+by+combining+boolean+conditions%0Aunsigned+int+doubletmatch(unsigned+int+arr%5B%5D,+unsigned+int+key)%0A%7B%0A++++unsigned+count+%3D+0%3B%0A++++for+(int+i%3D0%3B+i%3C1024+%3B+i%2B%2B)+%7B%0A++++++++//+gcc+uses+a+32-bit+OR+to+combine+setcc+results+even+with+-march%3Dcore2%0A++++++++//+causing+a+partial-register+stall.++(Even+with+old+gcc)%0A++++++++count+%2B%3D+(arr%5Bi%5D+%3D%3D+key)+%7C+(arr%5Bi%5D+%3D%3D+~key)%3B%0A++++++++//+clang+-m32+uses+dl+and+dh,+which+is+also+problematic%0A++++%7D%0A++++return+count%3B%0A%7D%0A%0A%0A//+compilers+don!&#39;t+spend+an+instruction+breaking+the+dep+on+the+setcc+destination,%0A//+even+with+-march%3Dbdver2+(Piledriver)+where+it!&#39;s+known+to+have+one.%0A//+This+doesn!&#39;t+usually+come+up,+since+trivial+functions+usually+inline.%0Achar+narrow_boolean(int+a)+%7B+return+a!!%3D5%3B+%7D%0A%0A%0A%23if+0%0A//+gcc+uses+setcc/movzx+for+these,+but+only+in+32-bit+mode.%0A//+I+think+because+of+the+extra+work+of+loading+args+from+the+stack%0A//+In+this+case+there!&#39;s+no+actual+register+pressure,%0A//+because+the+pointers+are+dead+by+the+time+we+need+them.%0Aint+foo(unsigned+int+%26a,+unsigned+int+%26b)+%7B%0A++++return+a+%2B+(a!!%3Db)%3B%0A%7D%0A%0Aextern+int+extfunc(unsigned)%3B%0Aunsigned+int+bar(unsigned+int+a,+unsigned+int+b)+%7B%0A++++a+*%3D+b%3B%0A++++b+*%3D+a%3B%0A++++a+*%3D+b%3B%0A++++b+*%3D+a%3B%0A++++//int+r+%3D+extfunc(a)%3B%0A++++return+a+%2B+(a!!%3Db)%3B%0A%7D%0A%23endif&#39;),l:&#39;5&#39;,n:&#39;0&#39;,o:&#39;C%2B%2B+source+%231&#39;,t:&#39;0&#39;)),k:37.151279838941626,l:&#39;4&#39;,n:&#39;0&#39;,o:&#39;&#39;,s:0,t:&#39;0&#39;),(g:!((h:compiler,i:(compiler:g71,filters:(b:&#39;0&#39;,commentOnly:&#39;0&#39;,directives:&#39;0&#39;,intel:&#39;0&#39;),options:&#39;-Wall+-O3++-mtune%3Dhaswell+-fno-tree-vectorize&#39;,source:1),l:&#39;5&#39;,n:&#39;0&#39;,o:&#39;x86-64+gcc+7.1+(Editor+%231,+Compiler+%231)&#39;,t:&#39;0&#39;)),k:30.12953615077989,l:&#39;4&#39;,m:100,n:&#39;0&#39;,o:&#39;&#39;,s:0,t:&#39;0&#39;),(g:!((h:compiler,i:(compiler:clang400,filters:(___0:(),b:&#39;0&#39;,commentOnly:&#39;0&#39;,directives:&#39;0&#39;,intel:&#39;0&#39;,jquery:&#39;3.2.1&#39;,length:1,prevObject:(___0:(sizzle1502600537666:(undefined:(legend:!(1590,0,&#39;1&#39;)))),length:1,prevObject:(___0:(jQuery321081933210614779781:(display:&#39;&#39;),sizzle1502600537666:(undefined:(legend:!(1590,0,&#39;1&#39;)))),length:1))),options:&#39;-Wall+-O3+-mtune%3Dhaswell++-fno-tree-vectorize+-fno-unroll-loops&#39;,source:1),l:&#39;5&#39;,n:&#39;0&#39;,o:&#39;x86-64+clang+4.0.0+(Editor+%231,+Compiler+%232)&#39;,t:&#39;0&#39;)),k:32.7191840102785,l:&#39;4&#39;,n:&#39;0&#39;,o:&#39;&#39;,s:0,t:&#39;0&#39;)),l:&#39;2&#39;,m:100,n:&#39;0&#39;,o:&#39;&#39;,t:&#39;0&#39;)),version:4" rel="noreferrer">(Godbolt compiler-explorer example)</a>, while clang uses xor-zero/cmp/setcc unless you combine multiple boolean conditions like <code>count += (a==b) | (a==~b)</code>.</p>
  
  <p>The xor/dec/setnz version runs at 3.0c per iteration on Skylake, Haswell, and Core2 (bottlenecked on <code>imul</code> throughput).  <code>xor</code>-zeroing breaks the dependency on the old value of <code>eax</code> on all out-of-order CPUs other than PPro/PII/PIII/early-Pentium-M (where it still avoids partial-register merging penalties but doesn't break the dep).  <a href="http://agner.org/optimize/" rel="noreferrer">Agner Fog's microarch guide describes this</a>.  Replacing the xor-zeroing with <code>mov eax,0</code> slows it down to one per 4.78 cycles on Core2: <a href="https://stackoverflow.com/questions/7031671/why-are-mov-ah-bh-and-mov-al-bl-together-much-faster-than-single-instruction-mo/35175245#35175245">2-3c stall (in the front-end?) to insert a partial-reg merging uop</a> when <code>imul</code> reads <code>eax</code> after <code>setnz al</code>.</p>
  
  <p>Also, I used <code>movzx eax, al</code> which defeats mov-elimination, just like <code>mov rax,rax</code> does.  (IvB, HSW, and SKL can rename <code>movzx eax, bl</code> with 0 latency, but Core2 can't).  This makes everything equal across Core2 / SKL, except for the partial-register behaviour.</p>
  
  <hr>
  
  <p>The Core2 behaviour is consistent with <a href="http://agner.org/optimize/" rel="noreferrer">Agner Fog's microarch guide</a>, but the HSW/SKL behaviour isn't.  From section 11.10 for Skylake, and same for previous Intel uarches:</p>
  
  <blockquote>
    <p>Different parts of a general purpose register can be stored in different temporary registers in order to remove false dependences.</p>
  </blockquote>
  
  <p>He unfortunately doesn't have time to do detailed testing for every new uarch to re-test assumptions, so this change in behaviour slipped through the cracks.</p>
  
  <p>Agner does describe a merging uop being inserted (without stalling) for high8 registers (AH/BH/CH/DH) on Sandybridge through Skylake, and for low8/low16 on SnB.  (I've unfortunately been spreading mis-information in the past, and saying that Haswell can merge AH for free.  I skimmed Agner's Haswell section too quickly, and didn't notice the later paragraph about high8 registers.  Let me know if you see my wrong comments on other posts, so I can delete them or add a correction.  I will try to at least find and edit my answers where I've said this.)</p>
  
  <hr>
  
  <p>My actual questions: <strong>How <em>exactly</em> do partial registers really behave on Skylake?</strong></p>
  
  <p><strong>Is everything the same from IvyBridge to Skylake, including the high8 extra latency?</strong></p>
  
  <p><a href="https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf#page=129" rel="noreferrer">Intel's optimization manual</a> is not specific about which CPUs have false dependencies for what (although it does mention that some CPUs have them), and leaves out things like reading AH/BH/CH/DH (high8 registers) adding extra latency even when they haven't been modified.</p>
  
  <p>If there's any P6-family (Core2/Nehalem) behaviour that Agner Fog's microarch guide doesn't describe, that would be interesting too, but I should probably limit the scope of this question to just Skylake or Sandybridge-family.</p>
  
  <hr>
  
  <p><strong>My Skylake test data</strong>, from putting <code>%rep 4</code> short sequences inside a small <code>dec ebp/jnz</code> loop that runs 100M or 1G iterations.  I measured cycles with Linux <code>perf</code> the same way as <a href="https://stackoverflow.com/questions/44169342/can-x86s-mov-really-be-free-why-cant-i-reproduce-this-at-all/44193770#44193770">in my answer here</a>, on the same hardware (desktop Skylake i7 6700k).</p>
  
  <p>Unless otherwise noted, each instruction runs as 1 fused-domain uop, using an ALU execution port.  (Measured with <a href="https://github.com/andikleen/pmu-tools" rel="noreferrer"><code>ocperf.py stat -e ...,uops_issued.any,uops_executed.thread</code></a>).  This detects (absence of) mov-elimination and extra merging uops.</p>
  
  <p>The "4 per cycle" cases are an extrapolation to the infinitely-unrolled case.  Loop overhead takes up some of the front-end bandwidth, but anything better than 1 per cycle is an indication that register-renaming avoided the <a href="https://en.wikipedia.org/wiki/Register_renaming#Data_hazards" rel="noreferrer">write-after-write output dependency</a>, and that the uop isn't handled internally as a read-modify-write.</p>
  
  <p><strong>Writing to AH only</strong>:  prevents the loop from executing from the loopback buffer (aka the Loop Stream Detector (LSD)).  Counts for <code>lsd.uops</code> are exactly 0 on HSW, and tiny on SKL (around 1.8k) and don't scale with the loop iteration count.  Probably those counts are from some kernel code.  When loops do run from the LSD, <code>lsd.uops ~= uops_issued</code> to within measurement noise.  Some loops alternate between LSD or no-LSD (e.g when they might not fit into the uop cache if decode starts in the wrong place), but I didn't run into that while testing this.</p>
  
  <ul>
  <li>repeated <code>mov ah, bh</code> and/or <code>mov ah, bl</code> runs at 4 per cycle.  It takes an ALU uop, so it's not eliminated like <code>mov eax, ebx</code> is.</li>
  <li>repeated <code>mov ah, [rsi]</code> runs at 2 per cycle (load throughput bottleneck).</li>
  <li>repeated <code>mov ah, 123</code> runs at 1 per cycle.  (A <a href="https://stackoverflow.com/questions/33666617/what-is-the-best-way-to-set-a-register-to-zero-in-x86-assembly-xor-mov-or-and/33668295#33668295">dep-breaking <code>xor eax,eax</code></a> inside the loop removes the bottleneck.)</li>
  <li><p>repeated <code>setz ah</code> or <code>setc ah</code> runs at 1 per cycle.  (A dep-breaking <code>xor eax,eax</code> lets it bottleneck on p06 throughput for <code>setcc</code> and the loop branch.)</p>
  
  <p><strong>Why does writing <code>ah</code> with an instruction that would normally use an ALU execution unit have a false dependency on the old value, while <code>mov r8, r/m8</code> doesn't (for reg or memory src)?</strong>  (And what about <code>mov r/m8, r8</code>?  Surely it doesn't matter which of the two opcodes you use for reg-reg moves?)</p></li>
  <li><p>repeated <code>add ah, 123</code> runs at 1 per cycle, as expected.</p></li>
  <li>repeated <code>add dh, cl</code> runs at 1 per cycle.</li>
  <li>repeated <code>add dh, dh</code> runs at 1 per cycle.</li>
  <li>repeated <code>add dh, ch</code> runs at 0.5 per cycle.  Reading [ABCD]H is special when they're "clean" (in this case, RCX is not recently modified at all).</li>
  </ul>
  
  <p><strong>Terminology</strong>: All of these leave AH (or DH) "<strong>dirty</strong>", i.e. in need of merging (with a merging uop) when the rest of the register is read (or in some other cases).  i.e. that AH is renamed separately from RAX, if I'm understanding this correctly.  "<strong>clean</strong>" is the opposite.  There are many ways to clean a dirty register, the simplest being <code>inc eax</code> or <code>mov eax, esi</code>.</p>
  
  <p><strong>Writing to AL only</strong>: These loops do run from the LSD: <code>uops_issue.any</code> ~= <code>lsd.uops</code>.</p>
  
  <ul>
  <li>repeated <code>mov al, bl</code> runs at 1 per cycle.  An occasional dep-breaking <code>xor eax,eax</code> per group lets OOO execution bottleneck on uop throughput, not latency.</li>
  <li>repeated <code>mov al, [rsi]</code> runs at 1 per cycle, as a micro-fused ALU+load uop.  (uops_issued=4G + loop overhead, uops_executed=8G + loop overhead).
  A dep-breaking <code>xor eax,eax</code> before a group of 4 lets it bottleneck on 2 loads per clock.</li>
  <li>repeated <code>mov al, 123</code> runs at 1 per cycle.</li>
  <li>repeated <code>mov al, bh</code> runs at 0.5 per cycle.  (1 per 2 cycles).  Reading [ABCD]H is special.</li>
  <li><code>xor eax,eax</code> + 6x <code>mov al,bh</code> + <code>dec ebp/jnz</code>: 2c per iter, bottleneck on 4 uops per clock for the front-end.</li>
  <li>repeated <code>add dl, ch</code> runs at 0.5 per cycle.  (1 per 2 cycles).  Reading [ABCD]H apparently creates extra latency for <code>dl</code>.</li>
  <li>repeated <code>add dl, cl</code> runs at 1 per cycle.</li>
  </ul>
  
  <p>I think a write to a low-8 reg behaves as a RMW blend into the full reg, like <code>add eax, 123</code> would be, but it doesn't trigger a merge if <code>ah</code> is dirty.  So (other than ignoring <code>AH</code> merging) it behaves the same as on CPUs that don't do partial-reg renaming at all.  It seems <code>AL</code> is never renamed separately from <code>RAX</code>?</p>
  
  <ul>
  <li><code>inc al</code>/<code>inc ah</code> pairs can run in parallel.</li>
  <li><code>mov ecx, eax</code> inserts a merging uop if <code>ah</code> is "dirty", but the actual <code>mov</code> is renamed.  This is what <a href="http://agner.org/optimize/" rel="noreferrer">Agner Fog describes</a> for IvyBridge and later.</li>
  <li>repeated <code>movzx  eax, ah</code> runs at one per 2 cycles.  (Reading high-8 registers after writing full regs has extra latency.)</li>
  <li><code>movzx ecx, al</code> has zero latency and doesn't take an execution port on HSW and SKL.  (Like what Agner Fog describes for IvyBridge, but he says HSW doesn't rename movzx).</li>
  <li><p><code>movzx ecx, cl</code> has 1c latency and takes an execution port.  (<a href="https://stackoverflow.com/questions/44169342/can-x86s-mov-really-be-free-why-cant-i-reproduce-this-at-all/44193770#44193770">mov-elimination never works for the <code>same,same</code> case</a>, only between different architectural registers.)</p>
  
  <p>A loop that inserts a merging uop every iteration can't run from the LSD (loop buffer)?</p></li>
  </ul>
  
  <p>I don't think there's anything special about AL/AH/RAX vs. B*, C*, DL/DH/RDX.  I have tested some with partial regs in other registers (even though I'm mostly showing <code>AL</code>/<code>AH</code> for consistency), and have never noticed any difference.</p>
  
  <p><strong>How can we explain all of these observations with a sensible model of how the microarch works internally?</strong></p>
  
  <hr>
  
  <p>Related:  Partial <strong>flag</strong> issues are different from partial <strong>register</strong> issues.  See <a href="https://stackoverflow.com/questions/36510095/inc-instruction-vs-add-1-does-it-matter/36510865#36510865">INC instruction vs ADD 1: Does it matter?</a> for some super-weird stuff with <code>shr r32,cl</code> (and even <code>shr r32,2</code> on Core2/Nehalem: don't read flags from a shift other than by 1).</p>
  
  <p>See also <a href="https://stackoverflow.com/questions/32084204/problems-with-adc-sbb-and-inc-dec-in-tight-loops-on-some-cpus">Problems with ADC/SBB and INC/DEC in tight loops on some CPUs</a> for partial-flag stuff in <code>adc</code> loops.</p>