Intel i7 loop performance anomaly (2013)
The call shifts the loop from being limited at the backend level (mostly uops not being retired by having to wait for memory) to being limited at the frontend level. The core event to look for here is `IDQ_UOPS_NOT_DELIVERED.CORE`, which tells us whether the frontend is delivering the full 4 uops per cycle it is able to. In the tight loop this is almost always the case, whereas in the call loop this is rarely the case.<p>CALL, RET, and JNE all share the same execution port (6 in Skylake), so it seems plausible that the added pressure in this port prevents speculative execution from continuing with the loop at the same rate as the tight loop. If you look at the execution port breakdowns of each loop, port 6 dominates in the call loop, whereas the tight loop is bottlenecked at port 4 (the port where stores go).<p>By delivering fewer uops per cycle, the pressure on the backend is eased. But this is a delicate balance. If you add another call, the loop becomes much slower than the tight loop.<p>You can get a similar effect by replacing `__asm__(&quot;call foo&quot;)` with<p><pre><code>    __asm__(&quot;jmp 1f\n1:\n&quot;);
    __asm__(&quot;jmp 1f\n1:\n&quot;);
</code></pre>
which consumes the same amount of port 6.