2 ms·
This is a uop cache issue. You can replace the entire body of the loop with some big NOPs as long as they are the same size as the existing instructions and see
by BeeOnRope 9y ago
This is a uop cache issue. You can replace the entire body of the loop with some big NOPs as long as they are the same size as the existing instructions and see the same effect: as soon as the loop spans two different 64-byte (!! see my reply below) aligned blocks, performance goes down. This holds even for larger iteration count if your LSD is disabled by microcode: asymptotically the fast loop approaches 1 cycle/iteration and the slow one 2/cycles per iteration.
This happens because the uop cache (also known as DSB) can only deliver instructions from one cache set at a time, and 64-byte (??) blocks map to different sets (that is, the uop cache holds a totally opaque decoded format unrelated to the instruction encoding, but the cache organization still depends on the original instruction sizes and alignment in the binary).
So if your loop spans 2 different 64-byte blocks, it will never run faster than 2 cycles per iteration. Normally, this is unlikely to be a problem: the vast majority of loops take more than 2 cycles per iteration anyways, and so this won't be a bottleneck. Loops that do take 1 cycle have, by definition, only a few instructions, so they aren't that likely to span a 32-byte boundary. This loop happened to have very long VEX-encoded instructions with offsets and so took up 28 bytes, even though it's still capable in running in less than 2 cycles, so it's got nearly a 50/50 chance to span a 64-byte boundary...
Now in principle compilers should come to the rescue here, but they are still busy aligning loops to 16-byte boundaries, which is the old advice that applied to the pre-uop cache days. So with 16-byte alignment, you pretty much have a 75% chance of getting lucky: this means that the only the 64N + 48 alignment ends up crossing a 64B boundary,so as it turns out, 16-byte alignment is still better than nothing: with no loop alignment at all, a 28-byte function often cross a 64B boundary (about 42% of the time for randomly distributed starting bytes).
The various other alignment effects are second-order issues, such as splitting a macro-fused branch slowing things down, or just adjusting the number of nops that have be executed.
- BeeOnRope 9y agoThe above comment originally used "32-byte boundary" everywhere it uses 64-byte now - the conventional wisdom was always that the uop cache operated on 32-byte boundaries only, and this is almost certainly true for Haswell and earlier. However, I noticed after a bit more testing that the critical boundary here is actually 64-bytes, not 32-bytes. That's odd as I always understood the boundary to be 32-bytes on Skylake as well, but either (a) it changed to 64-bytes in Skylake, or (b) there is a second-order effect at 64-bytes, and the uop cache can actually deliver instructions from two different ways in a single cycle (seems hard!). I note that Wikichip, which only has "so-so" accuracy does note: µOP Cache instruction window is now 64 Bytes (from 32) for Skylake-client https://en.wikichip.org/wiki/intel/microarchitectures/skylake_(client) https://en.wikichip.org/wiki/intel/microarchitectures/skylak... So maybe that's a little-noticed change.