In any case, I have to admit being wrong; port 6 has nothing to do with it, though the general principle of stalling the frontend to prevent the backend being overloaded still holds. For example, here's how to get the speedup without any change in control flow:
mov rcx, 400000000-1
align 32
nop
up:
align 32
add rcx, [counter]
align 32
dec rcx
align 32
jnz up
ret
The align directives here must resolve to an appropriate series of multi-byte nops. What we are doing here is to purposefully waste the uops cache. Each useful instruction comes at the beginning of a new 32-byte uop cache line, and the CPU cannot load more than 1 cache line per cycle. Effectively, we're limiting the rate of decoding by adding nops, which will not result in actual uops after decoding. I suspect the CALL and JMP instructions in the other variants were doing the same thing, but in a more obscure way. It's possible that CALL disables the LSD, which would matter in Sandy Bridge, but not in (updated) Skylake.As to why this actually makes things faster, I don't know. There are many more stalls owing to the reservation station being full in the tight loop:
1,348,491,506 resource_stalls_any
1,694,613,980 uops_issued_stall_cycles
759,763,767 uops_executed_stall_cycles
994,974,421 uops_retired_stall_cycles
vs 109,300,935 resource_stalls_any
120,303,318 uops_issued_stall_cycles
164,322,430 uops_executed_stall_cycles
15,834,456 uops_retired_stall_cycles
But how does this translate to the observed performance penalty? One clue lies in the port breakdown: 756,686,062 uops_dispatched_port_port_0
782,579,771 uops_dispatched_port_port_1
279,387,822 uops_dispatched_port_port_2
314,214,218 uops_dispatched_port_port_3
1,994,313,938 uops_dispatched_port_port_4
807,514,909 uops_dispatched_port_port_5
1,050,884,205 uops_dispatched_port_port_6
209,316,457 uops_dispatched_port_port_7
vs 640,957,968 uops_dispatched_port_port_0
637,621,266 uops_dispatched_port_port_1
399,224,647 uops_dispatched_port_port_2
400,205,411 uops_dispatched_port_port_3
1,610,294,604 uops_dispatched_port_port_4
643,780,748 uops_dispatched_port_port_5
848,142,785 uops_dispatched_port_port_6
470,700 uops_dispatched_port_port_7
Focus on port 4, the only port available for memory stores. It does nothing else, so ideally we would expect ~400000000 uops to be dispatched to port 4. Instead, we have ~4x that for the fast loop, and ~5x that for the slow loop. These, presumably, are speculative stores that had to be invalidated. By dispatching fewer instructions that we can handle per cycle, there is less speculation and thus less wasted work?