我正在嘗試使用我的 Intel i7-10700 和 ubuntu 20.04 驗證可以在同一時鐘周期內解碼兩個可熔斷對的結論。
測驗代碼如下排列,復制8000次,避免LSD和DSB的影響(主要使用MITE)。
ALIGN 32
.loop_1:
dec ecx
jge .loop_2
.loop_2:
dec ecx
jge .loop_3
.loop_3:
dec ecx
jge .loop_4
.loop_4:
.loop_5:
dec ecx
jge .loop_6
測驗結果表明在一個回圈中只有一對融合。( r479 div r1002479 )
Performance counter stats for process id '22597':
120,459,876,711 cycles
35,514,146,968 instructions # 0.29 insn per cycle
17,792,584,278 r479 # r479: Number of uops delivered
# to Instruction Decode Queue (IDQ) from MITE path
50,968,497 r4002479
17,756,894,879 r1002479 # r1002479: Cycles MITE is delivering any Uop
26.444208448 seconds time elapsed
我不認為阿格納的結論是錯誤的。因此,我的性能使用有什么問題,還是我沒有在代碼中找到見解?
uj5u.com熱心網友回復:
在 Haswell 和更高版本上,是的。在常春藤橋和更早的地方,沒有。
在 Ice Lake 和更高版本上,Agner Fog 表示宏融合是在解碼后立即完成的,而不是在解碼器中進行,后者要求預解碼器相應地將正確的 x86 機器代碼塊發送給解碼器。(并且 Ice Lake 的限制略有不同:與以前的 CPU 模型不同,具有記憶體運算元的指令不能融合。具有立即運算元的指令可以融合。)因此,在 Ice Lake 上,宏融合不會讓解碼器處理超過 5 條指令每個時鐘。
Wikichip聲稱在 Ice Lake 上每個時鐘只能進行 1 次宏融合,但這可能是不正確的。Harold在 Rocket Lake 上用我的微基準測驗并發現與 Skylake 相同的結果。(Rocket Lake使用 Cypress Cove 內核,這是 Sunny Cove 的一個變體,反向移植到 14nm 工藝,因此在這方面它很可能與 Ice Lake 相同。)
你的結果表明,uops_issued.any約一半instructions,因此,你都看到大多數對宏融合。(您也可以查看uops_retired.macro_fusedperf 事件。順便說一句,現代perf對大多數特定于 uarch 的事件都有符號名稱:用于perf list查看它們。)
盡管如此,解碼器仍然會在 Skylake 衍生的微架構上每個時鐘產生多達四個甚至五個 uop,即使它們只進行兩次宏融合。您沒有查看MITE 處于活動狀態的周期數,因此您在大部分時間都看不到執行停止,直到 ROB/RS 中有空間用于 4 個 uops 的問題組。這為 MITE 的解碼組在 IDQ 中開辟了空間。
您的回圈中還有其他三個瓶頸:
回圈攜帶依賴通過
dec ecx:只有 1 個/時鐘,因為每個dec都必須等待前一個的結果準備好。只有一個采取分支可以執行每個周期(在埠6),和
dec/jge取幾乎每次,除1在2 ^ 32時ECX的dec之前為0。
埠 0 上的另一個分支執行單元僅處理預測未采用的分支。 https://www.realworldtech.com/haswell-cpu/4/顯示了布局但沒有提到這個限制;Agner Fog的microarch指南確實如此。分支預測:即使跳轉到下一條指令,在架構上是 NOP,CPU 也沒有特殊情況。緩慢的 jmp 指令(因為沒有理由讓真正的代碼這樣做,除了
call 0/pop至少對于回傳地址預測器堆疊是特殊情況。)這就是為什么您在每個時鐘執行的指令明顯少于一條指令,更不用說每個時鐘執行一個uop 了。
每個時鐘 2 個融合的作業演示
令人驚訝的對我來說,螨沒去解碼獨立的test和jcc,因為它做了兩個融合在同一個周期。我猜解碼器針對填充 uop 快取進行了優化。(對 Sandybridge / IvyBridge 的類似影響是,如果解碼組的最終 uop 可能是可熔斷的,例如dec,解碼器將在該周期僅產生 3 個uops ,以預期可能會融合dec下一個周期。至少在 SnB/ IvB 解碼器每個周期只能進行 1 次融合,如果同一解碼組中有另一對,則將解碼單獨的 ALU jcc uop。這里,SKL 選擇不解碼單獨的testuop(jcc和另一個test),在制作了兩個之后融合。)
global _start
_start:
mov ecx, 100000000
ALIGN 32
.loop:
%rep 399 ; the loop branch makes 400 total
test ecx, ecx
jz .exit_loop ; many of these will be 6-byte jcc rel32
%endrep
dec ecx
jnz .loop
.exit_loop:
mov eax, 231
syscall ; exit_group(EDI)
在 i7-6700k Skylake 上,僅用于用戶空間的性能計數器:
$ nasm -felf64 fusion.asm && ld fusion.o -o fusion # static executable
$ taskset -c 3 perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,uops_executed.thread,idq.all_mite_cycles_any_uops,idq.mite_uops -r2 ./fusion
Performance counter stats for './fusion' (2 runs):
5,165.34 msec task-clock # 1.000 CPUs utilized ( - 0.01% )
0 context-switches # 0.000 /sec
0 cpu-migrations # 0.000 /sec
1 page-faults # 0.194 /sec
20,130,230,894 cycles # 3.897 GHz ( - 0.04% )
80,000,001,586 instructions # 3.97 insn per cycle ( - 0.00% )
40,000,677,865 uops_issued.any # 7.744 G/sec ( - 0.00% )
40,000,602,728 uops_executed.thread # 7.744 G/sec ( - 0.00% )
20,100,486,534 idq.all_mite_cycles_any_uops # 3.891 G/sec ( - 0.00% )
40,000,261,852 idq.mite_uops # 7.744 G/sec ( - 0.00% )
5.165605 - 0.000716 seconds time elapsed ( - 0.01% )
Not-taken branches aren't a bottleneck, perhaps because my loop is big enough to defeat the DSB (uop cache), but not too big to defeat branch prediction. (Actually, the JCC erratum mitigation on Skylake will definitely defeat the DSB: if everything is a macro-fused branch, there will be one touching the end of every 32-byte region. Only if we start introducing NOPs or other instructions between branches will the uop cache be able to operate.)
We can see that everything was fused (80G instructions in 40G uops) and executing at 2 test-and-branch uops per clock (20G cycles). Also that MITE is delivering uops every cycle, 20G MITE cycles. And what it does deliver is apparently 2 uops per cycle, at least on average.
A test with alternating groups of NOPs and not-taken branches might be good to see what happens when there's room for the IDQ to accept more uops from MITE, to see if it will send non-fused test and JCC uops to the IDQ.
Further tests:
Backwards jcc rel8 for all the branches made no difference, same perf results:
%assign i 0
%rep 399 ; the loop branch makes 400 total
.dummy% i:
test ecx, ecx
jz .dummy % i
%assign i i 1
%endrep
MITE throughput: alternating groups of NOPs and macro-fused branches
The NOPs still need to get decoded, but the back-end can blaze through them. This makes total MITE throughput the only bottleneck, instead of being limited to 2 uops / clock regardless of how many MITE could produce.
global _start
_start:
mov ecx, 100000000
ALIGN 32
.loop:
%assign i 0
%rep 10
%rep 8
.dummy% i:
test ecx, ecx
jz .dummy % i
%assign i i 1
%endrep
times 24 nop
%endrep
dec ecx
jnz .loop
.exit_loop:
mov eax, 231
syscall ; exit_group(EDI)
Performance counter stats for './fusion':
2,594.14 msec task-clock # 1.000 CPUs utilized
0 context-switches # 0.000 /sec
0 cpu-migrations # 0.000 /sec
1 page-faults # 0.385 /sec
10,112,077,793 cycles # 3.898 GHz
40,200,000,813 instructions # 3.98 insn per cycle
32,100,317,400 uops_issued.any # 12.374 G/sec
8,100,250,120 uops_executed.thread # 3.123 G/sec
10,100,772,325 idq.all_mite_cycles_any_uops # 3.894 G/sec
32,100,146,351 idq.mite_uops # 12.374 G/sec
2.594423202 seconds time elapsed
2.593606000 seconds user
0.000000000 seconds sys
So it seems MITE couldn't keep up with 4-wide issue. The blocks of 8 branches are making the decoders produce significantly less than 5 uops per clock; probably only 2 like we were seeing for longer runs of test/jcc.
24 nops can decode in
Reducing to groups of 3 test/jcc and 29 nop gets it down to 8.607 Gcycles for MITE active 8.600 cycles, with 32.100G MITE uops. (3.099 G uops_retired.macro_fused, with the .1 coming from the loop branch.) Still not saturating the front-end with 4.0 uops per clock, like I was hoping it might with a macro-fusion at the end of one decode group.
It is hitting 4.09 IPC, so at least the decoders and issue bottleneck are ahead of where they'd be with no macro-fusion.
(Best case for macro-fusion is 6.0 IPC, with 2 fusions per cycle and 2 other uops from non-fusing instructions. That's separate from unfused-domain back-end uop throughput limits via micro-fusion, see this test for ~7 uops_executed.thread per clock.)
Even %rep 2 test/JCC hurts throughput, which seems to indicate that it just stops decoding after making 2 fusions, not even decoding 2 or 3 more NOPs after that. (For some lower NOP counts, we get some uop-cache activity because the outer rep count isn't big enough to totally fill up the uop cache.)
You can test this in a shell loop like for NOPS in {0..20}; do nasm ... -DNOPS=$NOPS ... with the source using times NOPS nop.
There are some plateau/step effects in total cycles vs. number of NOPS for %rep 2, so maybe the two test/JCC uops are decoding at the end of a group, with 1, 2, or 3 NOPs before them. (But it's not super consistent, especially for lower numbers of NOPS. But NOPS=16, 17 and 18 are all right around 5.22 Gcycles, with 14 and 15 both at 4.62 Gcycles.)
There are a lot of possibly-relevant perf counters if we want to really get into what's going on, e.g. idq_uops_not_delivered.cycles_fe_was_ok (cycles where the issue stage got 4 uops, or where the back-end was stalled so it wasn't the front-end's fault.)
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/357363.html
