[英]How are x86 uops scheduled, exactly?
現代 x86 CPU 將傳入的指令流分解為微操作 (uops 1 ),然后在它們的輸入准備就緒時亂序調度這些 uops。 雖然基本思想很清楚,但我想知道如何安排就緒指令的具體細節,因為它會影響微優化決策。
例如,以下面的玩具循環2為例:
top:
lea eax, [ecx + 5]
popcnt eax, eax
add edi, eax
dec ecx
jnz top
這基本上實現了循環(具有以下對應關系: eax -> total, c -> ecx
):
do {
total += popcnt(c + 5);
} while (--c > 0);
我熟悉通過查看uop分解,依賴鏈延遲等來優化任何小循環的過程。 在上面的循環中,我們只有一個攜帶依賴鏈: dec ecx
。 循環的前三個指令( lea
、 popcnt
、 add
)是依賴鏈的一部分,每個循環都popcnt
開始。
最后的dec
和jne
融合在一起。 所以我們總共有 4 個融合域 uops,一個只有循環攜帶的依賴鏈,延遲為 1 個周期。 因此,根據該標准,循環似乎可以執行 1 個循環/迭代。
但是,我們也應該查看端口壓力:
lea
可以在端口 1 和 5 上執行add
可以在端口 0、1、5 和 6 上執行jnz
在端口 6 上執行因此,要達到 1 個循環/迭代,您幾乎需要執行以下操作:
lea
必須在端口 5 上執行(而不是在端口 1 上)add
必須在端口 0 上執行,絕不能在它可以執行的其他三個端口中的任何一個上執行jnz
無論如何只能在端口 6 上執行這是很多條件! 如果指令只是隨機安排的,您可能會得到更糟糕的吞吐量。 例如,75% 的add
將進入端口 1、5 或 6,這將使popcnt
、 lea
或jnz
延遲一個周期。 同樣,對於可以去 2 個端口的lea
,一個與popcnt
共享。
另一方面,IACA 報告的結果非常接近最佳,每次迭代 1.05 個周期:
Intel(R) Architecture Code Analyzer Version - 2.1
Analyzed File - l.o
Binary Format - 64Bit
Architecture - HSW
Analysis Type - Throughput
Throughput Analysis Report
--------------------------
Block Throughput: 1.05 Cycles Throughput Bottleneck: FrontEnd, Port0, Port1, Port5
Port Binding In Cycles Per Iteration:
---------------------------------------------------------------------------------------
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 |
---------------------------------------------------------------------------------------
| Cycles | 1.0 0.0 | 1.0 | 0.0 0.0 | 0.0 0.0 | 0.0 | 1.0 | 0.9 | 0.0 |
---------------------------------------------------------------------------------------
N - port number or number of cycles resource conflict caused delay, DV - Divider pipe (on port 0)
D - Data fetch pipe (on ports 2 and 3), CP - on a critical path
F - Macro Fusion with the previous instruction occurred
* - instruction micro-ops not bound to a port
^ - Micro Fusion happened
# - ESP Tracking sync uop was issued
@ - SSE instruction followed an AVX256 instruction, dozens of cycles penalty is expected
! - instruction not supported, was not accounted in Analysis
| Num Of | Ports pressure in cycles | |
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | |
---------------------------------------------------------------------------------
| 1 | | | | | | 1.0 | | | CP | lea eax, ptr [ecx+0x5]
| 1 | | 1.0 | | | | | | | CP | popcnt eax, eax
| 1 | 0.1 | | | | | 0.1 | 0.9 | | CP | add edi, eax
| 1 | 0.9 | | | | | | 0.1 | | CP | dec ecx
| 0F | | | | | | | | | | jnz 0xfffffffffffffff4
它幾乎反映了我上面提到的必要的“理想”調度,有一個小偏差:它顯示了在 10 個周期中的 1 個周期中add
從lea
竊取端口 5。 它也不會知道,融合的分支要去口6,因為預計拍攝,所以它把大部分的微指令為分支端口0,而大部分的微指令的的add
端口6,而反之亦然。
目前尚不清楚 IACA 報告的超過最優值的額外 0.05 個循環是一些深入、准確分析的結果,還是它使用的算法的洞察力較低的結果,例如,分析固定數量的循環的循環,或者只是一個錯誤或什么。 對於它認為將進入非理想端口的 uop 的 0.1 部分也是如此。 也不清楚是否有人解釋了另一個 - 我認為錯誤分配端口 1 的 10 次會導致每次迭代的循環計數為 11/10 = 1.1 個循環,但我還沒有計算出實際的下游結果 - 也許平均影響較小。 或者它可能只是四舍五入(0.05 == 0.1 到 1 個小數位)。
那么現代 x86 CPU 是如何調度的呢? 特別是:
add
和lea
),它如何決定選擇哪個端口?讓我們在 Skylake 上測量一些實際結果,以檢查哪些答案解釋了實驗證據,因此這里是我的 Skylake 盒子上的一些真實世界測量結果(來自perf
)。 令人困惑的是,我將轉而將imul
用於我的“僅在一個端口上執行”指令,因為它有許多變體,包括 3 參數版本,允許您對源和目標使用不同的寄存器。 這在嘗試構建依賴鏈時非常方便。 它還避免了popcnt
具有的整個“對目的地的錯誤依賴”。
讓我們先看看指令相對獨立的簡單 (?) 情況——除了像循環計數器這樣的瑣碎鏈之外,沒有任何依賴鏈。
這是一個帶有輕微壓力的 4 uop 循環(僅執行 3 個 uop)。 所有指令都是獨立的(不共享任何來源或目的地)。 add
原則上可以竊取imul
所需的p1
或 dec 所需的p6
:
instr p0 p1 p5 p6
xor (elim)
imul X
add X X X X
dec X
top:
xor r9, r9
add r8, rdx
imul rax, rbx, 5
dec esi
jnz top
The results is that this executes with perfect scheduling at 1.00 cycles / iteration:
560,709,974 uops_dispatched_port_port_0 ( +- 0.38% )
1,000,026,608 uops_dispatched_port_port_1 ( +- 0.00% )
439,324,609 uops_dispatched_port_port_5 ( +- 0.49% )
1,000,041,224 uops_dispatched_port_port_6 ( +- 0.00% )
5,000,000,110 instructions:u # 5.00 insns per cycle ( +- 0.00% )
1,000,281,902 cycles:u
( +- 0.00% )
正如預期的那樣, p1
和p6
分別被imul
和dec/jnz
充分利用,然后add
問題大約占剩余可用端口的一半。 粗略地注意 - 實際比率是 56% 和 44%,並且這個比率在運行中非常穩定(注意+- 0.49%
變化)。 如果我調整循環對齊,拆分會發生變化(32B 對齊為 53/46,32B+4 對齊更像是 57/42)。 現在,除了imul
在循環中的位置之外,我們什么都不改變:
top:
imul rax, rbx, 5
xor r9, r9
add r8, rdx
dec esi
jnz top
然后突然p0
/ p5
分裂正好是 50%/50%,有 0.00% 的變化:
500,025,758 uops_dispatched_port_port_0 ( +- 0.00% )
1,000,044,901 uops_dispatched_port_port_1 ( +- 0.00% )
500,038,070 uops_dispatched_port_port_5 ( +- 0.00% )
1,000,066,733 uops_dispatched_port_port_6 ( +- 0.00% )
5,000,000,439 instructions:u # 5.00 insns per cycle ( +- 0.00% )
1,000,439,396 cycles:u ( +- 0.01% )
所以這已經很有趣了,但很難說到底發生了什么。 也許確切的行為取決於循環入口處的初始條件,並且對循環內的排序很敏感(例如,因為使用了計數器)。 這個例子表明正在發生的不僅僅是“隨機”或“愚蠢”的調度。 特別是,如果您只是從循環中消除imul
指令,則會得到以下結果:
330,214,329 uops_dispatched_port_port_0 ( +- 0.40% )
314,012,342 uops_dispatched_port_port_1 ( +- 1.77% )
355,817,739 uops_dispatched_port_port_5 ( +- 1.21% )
1,000,034,653 uops_dispatched_port_port_6 ( +- 0.00% )
4,000,000,160 instructions:u # 4.00 insns per cycle ( +- 0.00% )
1,000,235,522 cycles:u ( +- 0.00% )
在這里, add
現在大致均勻地分布在p0
、 p1
和p5
- 因此imul
的存在確實影響了add
調度:它不僅僅是某些“避免端口 1”規則的結果。
請注意,總端口壓力僅為 3 uop/周期,因為xor
是一個歸零習語,並在重命名器中被消除。 讓我們嘗試使用 4 uop 的最大壓力。 我希望上面啟動的任何機制也能夠完美地安排它。 我們只將xor r9, r9
更改為xor r9, r10
,因此它不再是歸零習語。 我們得到以下結果:
top:
xor r9, r10
add r8, rdx
imul rax, rbx, 5
dec esi
jnz top
488,245,238 uops_dispatched_port_port_0 ( +- 0.50% )
1,241,118,197 uops_dispatched_port_port_1 ( +- 0.03% )
1,027,345,180 uops_dispatched_port_port_5 ( +- 0.28% )
1,243,743,312 uops_dispatched_port_port_6 ( +- 0.04% )
5,000,000,711 instructions:u # 2.66 insns per cycle ( +- 0.00% )
1,880,606,080 cycles:u ( +- 0.08% )
哎呀! 調度程序沒有在p0156
均勻調度所有內容, p0156
未充分利用p0
(它只執行約 49% 的周期),因此p1
和p6
被過度訂閱,因為它們正在執行所需的imul
和dec/jnz
。 我認為這種行為與 hayesti 在他們的回答中指出的基於計數器的壓力指示器一致,並且在發布時將 uops分配給端口,而不是在hayesti 和 Peter Cordes 提到的執行時。 行為3使得執行最舊的就緒 uops規則幾乎沒有那么有效。 如果 uops 沒有綁定到有問題的執行端口,而是在執行時綁定,那么這個“最古老的”規則將在一次迭代后解決上面的問題——一旦一個imul
和一個dec/jnz
被阻止進行一次迭代,他們將總是比競爭的xor
更舊並add
指令,所以應該總是首先安排。 不過,我正在學習的一件事是,如果端口是在發布時分配的,則此規則無濟於事,因為端口是在發布時預先確定的。 我想它仍然有助於支持作為長依賴鏈一部分的指令(因為它們往往會落后),但這並不是我認為的萬能葯。
這也似乎是一個解釋上述結果: p0
被分配更多的壓力比它確實有因為dec/jnz
組合在理論上可以上執行p06
。 事實上,因為分支被預測采取它只去p6
,但也許該信息不能提供給壓力平衡算法,所以計數器往往會看到p016
上的相等壓力,這意味着add
和xor
得到傳播與最優不同。
也許我們可以通過稍微展開循環來測試這一點,這樣jnz
就不是一個因素......
1好的,它寫得正確μops ,但這會扼殺搜索能力並實際輸入“μ”字符,我通常求助於從網頁復制粘貼該字符。
2我最初在循環中使用imul
而不是popcnt
,但令人難以置信的是,_IACA 不支持它_!
3請注意,我並不是在暗示這是一個糟糕的設計或任何東西 - 可能有很好的硬件原因導致調度程序無法在執行時輕松做出所有決定。
您的問題很棘手,原因如下:
不過,我會盡量回答...
當保留站中准備好多個微指令時,它們按什么順序被調度到端口?
它應該是最古老的 [見下文],但您的里程可能會有所不同。 P6 微體系結構(在 Pentium Pro、2 和 3 中使用)使用帶有五個調度程序(每個執行端口一個)的保留站; 調度程序使用優先級指針作為開始掃描准備好要調度的 uops 的位置。 它只是偽 FIFO,因此完全有可能並不總是安排最舊的就緒指令。 在 NetBurst 微體系結構(在 Pentium 4 中使用)中,他們放棄了統一保留站,而是使用兩個 uop 隊列。 這些是適當的折疊優先級隊列,因此可以保證調度程序獲得最舊的就緒指令。 核心架構返回到一個保留站,我會冒險猜測他們使用了折疊優先級隊列,但我找不到證實這一點的來源。 如果有人有明確的答案,我會全神貫注。
當一個 uop 可以去多個端口時(如上例中的 add 和 lea),它是如何決定選擇哪個端口的?
這很難知道。 我能找到的最好的是來自英特爾的專利,描述了這種機制。 本質上,它們為每個具有冗余功能單元的端口保留一個計數器。 當 uops 離開前端到保留站時,它們會被分配一個調度端口。 如果必須在多個冗余執行單元之間做出決定,則使用計數器來平均分配工作。 計數器隨着微指令分別進入和離開保留站而遞增和遞減。
當然,這只是一種啟發式方法,並不能保證完美的無沖突時間表,但是,我仍然可以看到它與您的玩具示例一起使用。 只能到達一個端口的指令最終會影響調度程序將“限制較少”的 uops 分派到其他端口。
在任何情況下,專利的存在並不一定意味着該想法被采用(盡管如此說,其中一位作者也是奔騰 4 的技術負責人,所以誰知道呢?)
如果任何答案都涉及在 uops 中選擇最舊的概念,那么它是如何定義的? 自交付給 RS 以來的年齡? 准備好后的年齡? 關系是怎么斷的? 程序順序有沒有出現過?
由於微指令是按順序插入保留站的,這里最老的確實是指它進入保留站的時間,即程序順序最舊。
順便說一句,我會對那些 IACA 結果持懷疑態度,因為它們可能無法反映真實硬件的細微差別。 在 Haswell 上,有一個名為uops_executed_port的硬件計數器,它可以告訴您線程中有多少個周期是端口 0-7 的uops問題。 也許您可以利用這些來更好地了解您的程序?
這是我在 Skylake 上發現的,從uops 在發布時間(即,當它們被發布到 RS 時)而不是在調度時間(即,在它們被發送執行時)分配給端口的角度來看。 . 在我明白港口決定是在派送時做出的。
我進行了各種測試,試圖隔離可以進入p0156
的add
操作序列和僅進入端口 0 的imul
操作。一個典型的測試是這樣的:
mov eax, [edi]
mov eax, [edi]
mov eax, [edi]
mov eax, [edi]
... many more mov instructions
mov eax, [edi]
mov eax, [edi]
mov eax, [edi]
mov eax, [edi]
imul ebx, ebx, 1
imul ebx, ebx, 1
imul ebx, ebx, 1
imul ebx, ebx, 1
add r9, 1
add r8, 1
add ecx, 1
add edx, 1
add r9, 1
add r8, 1
add ecx, 1
add edx, 1
add r9, 1
add r8, 1
add ecx, 1
add edx, 1
mov eax, [edi]
mov eax, [edi]
mov eax, [edi]
mov eax, [edi]
... many more mov instructions
mov eax, [edi]
mov eax, [edi]
mov eax, [edi]
mov eax, [edi]
基本上有很長的mov eax, [edi]
指令引入,它們只在p23
發出,因此不會阻塞指令使用的端口(我也可以使用nop
指令,但測試將是一個有點不同,因為nop
不向 RS 發出問題)。 接下來是“有效載荷”部分,這里由 4 個imul
和 12 個add
,然后是更多虛擬mov
指令的引出部分。
首先,讓我們看看上面 hayesti 鏈接的專利,他描述了其基本思想:每個端口的計數器,用於跟蹤分配給端口的 uop 總數,用於對端口分配進行負載平衡。 看看專利描述中包含的這張表:
該表用於為專利中討論的 3-wide 架構的問題組中的 3-uop 在p0
或p1
之間進行選擇。 請注意,行為取決於uop 在 group 中的位置,並且有 4個基於計數的規則1 ,它們以合乎邏輯的方式散布 uop。 特別是,在整個組被分配到未充分使用的端口之前,計數需要為 +/- 2 或更大。
讓我們看看我們是否可以在 Sklake 上觀察“在問題組中的位置”問題的行為。 我們使用單個add
的有效負載,例如:
add edx, 1 ; position 0
mov eax, [edi]
mov eax, [edi]
mov eax, [edi]
...我們將其在 4 指令卡盤內滑動,例如:
mov eax, [edi]
add edx, 1 ; position 1
mov eax, [edi]
mov eax, [edi]
... 以此類推,測試問題組2 中的所有四個位置。 當 RS 已滿(包含mov
指令)但沒有任何相關端口的端口壓力時,這將顯示以下內容:
add
指令轉到p5
或p6
,選擇的端口通常隨着指令變慢而交替(即,偶數位置的add
指令轉到p5
,奇數位置的add
指令轉到p6
)。add
指令也轉到p56
- 第一個沒有轉到的兩個中的那個。add
指令開始在p0156
周圍平衡, p5
和p6
通常在前面,但總體上相當均勻(即p56
和其他兩個端口之間的差距沒有增加)。 接下來,我看看如果用imul
操作加載p1
會發生什么,然后首先在一堆add
操作中:
imul ebx, ebx, 1
imul ebx, ebx, 1
imul ebx, ebx, 1
imul ebx, ebx, 1
add r9, 1
add r8, 1
add ecx, 1
add edx, 1
add r9, 1
add r8, 1
add ecx, 1
add edx, 1
add r9, 1
add r8, 1
add ecx, 1
add edx, 1
結果表明調度程序處理得很好——所有imul
被調度到p1
(如預期的那樣),然后沒有后續的add
指令進入p1
,而是在p056
周圍傳播。 所以這里的調度運行良好。
當然,當情況相反,並且imul
序列在add
s 之后出現時, p1
在imul
s 命中之前加載了它的add
份額。 這是端口分配在發布時按順序發生的結果,因為在調度add
時沒有“向前看”和查看imul
的機制。
總體而言,調度程序看起來在這些測試用例中做得很好。
它沒有解釋在更小、更緊密的循環中會發生什么,如下所示:
sub r9, 1
sub r10, 1
imul ebx, edx, 1
dec ecx
jnz top
就像我的問題中的示例 4一樣,盡管有兩個sub
指令應該能夠在每個周期轉到p0
,但該循環僅在大約 30% 的周期內填充p0
。 p1
和p6
被超額訂閱,每個迭代執行 1.24 uop(1 是理想的)。 我無法對在此答案頂部運行良好的示例與壞循環之間的差異進行三角測量 - 但仍有許多想法可以嘗試。
我確實注意到沒有指令延遲差異的示例似乎不會受到這個問題的影響。 例如,這是另一個具有“復雜”端口壓力的 4-uop 循環:
top:
sub r8, 1
ror r11, 2
bswap eax
dec ecx
jnz top
uop圖如下:
instr p0 p1 p5 p6
sub X X X X
ror X X
bswap X X
dec/jnz X
因此sub
必須始終轉到p15
,如果要解決問題,則與bswap
共享。 他們是這樣:
'./sched-test2' 的性能計數器統計信息(2 次運行):
999,709,142 uops_dispatched_port_port_0 ( +- 0.00% )
999,675,324 uops_dispatched_port_port_1 ( +- 0.00% )
999,772,564 uops_dispatched_port_port_5 ( +- 0.00% )
1,000,991,020 uops_dispatched_port_port_6 ( +- 0.00% )
4,000,238,468 uops_issued_any ( +- 0.00% )
5,000,000,117 instructions:u # 4.99 insns per cycle ( +- 0.00% )
1,001,268,722 cycles:u ( +- 0.00% )
如此看來,該問題可能與指令延遲(當然,也有實例之間的其他差異)。 這是在這個類似問題中提出的。
1該表有 5 個規則,但 0 和 -1 計數的規則是相同的。
2當然,我不能確定問題組從哪里開始和結束,但不管我們在滑下四個指令時測試四個不同的位置(但標簽可能是錯誤的)。 我也不確定問題組的最大大小是 4 - 管道的早期部分更寬 - 但我相信它是,並且一些測試似乎表明它是(具有 4 uop 倍數的循環顯示一致的調度行為)。 在任何情況下,結論都適用於不同的調度組大小。
最近英特爾微架構上基本塊的准確吞吐量預測[^1] 的第 2.12 節解釋了端口的分配方式,盡管它未能解釋問題描述中的示例 4。 我也沒有弄清楚延遲在端口分配中扮演什么角色。
之前的工作 [19, 25, 26] 已經確定了單個指令的微操作可以使用的端口。 然而,對於可以使用多個端口的微操作,處理器如何選擇實際端口是未知的。 我們使用微基准對端口分配算法進行了逆向工程。 下面,我們將描述我們對具有 8 個端口的 CPU 的發現; 此類 CPU 目前使用最為廣泛。
當重命名程序向調度程序發出 µop 時,就會分配端口。 在單個周期中,最多可以發出 4 個 µop。 在下文中,我們將循環中 µop 的位置稱為發布槽; 例如,一個周期中發出的最舊指令將占用發出槽 0。
µop 分配的端口取決於它的發布槽和分配給尚未執行且在前一個周期發布的 µop 的端口。
在下文中,我們將只考慮可以使用多個端口的微操作。 對於給定的 µop m,讓 $P_{min}$ 是 m 可以使用的端口中分配到的最少非執行 µop 的端口。 令 $P_{min'}$ 是目前使用量第二小的端口。 如果使用量最小(或分別為第二小)的端口之間存在聯系,則讓 $P_{min}$(或 $P_{min'}$)是這些端口中端口號最高的端口(這種選擇的原因可能是編號較大的端口連接到較少的功能單元)。 如果 $P_{min}$ 和 $P_{min'}$ 之間的差值大於或等於 3,我們將 $P_{min'}$ 設置為 $P_{min}$。
發布時隙 0 和 2 中的微操作分配給端口 $P_{min}$ 發布時隙 1 和 3 中的微操作分配給端口 $P_{min'}$。
一個特例是可以使用端口 2 和端口 3 的 µops。這些端口由處理內存訪問的 µops 使用,並且兩個端口都連接到相同類型的功能單元。 對於此類微操作,端口分配算法在端口 2 和端口 3 之間交替。
我試圖找出 $P_{min}$ 和 $P_{min'}$ 是否在線程之間共享(超線程),即一個線程是否會影響同一內核中另一個線程的端口分配。
只需將 BeeOnRope 的答案中使用的代碼拆分為兩個線程即可。
thread1:
.loop:
imul rax, rbx, 5
jmp .loop
thread2:
mov esi,1000000000
.top:
bswap eax
dec esi
jnz .top
jmp thread2
其中指令bswap
可以在端口 1 和 5 上執行, imul r64, R64, i
在端口 1 上執行。如果計數器在線程之間共享,您會看到bswap
在端口 5 上執行,而imul
在端口 1 上執行。
實驗記錄如下,其中線程1上的端口P0和P5和線程2上的p0應該記錄了少量非用戶數據,但不妨礙得出結論。 從數據可以看出,線程2的bswap
指令在端口P1和P5之間交替執行,沒有放棄P1。
港口 | 線程 1 活動周期 | 線程 2 個活動周期 |
---|---|---|
P0 | 63,088,967 | 68,022,708 |
P1 | 180,219,013,832 | 95,742,764,738 |
P5 | 63,994,200 | 96,291,124,547 |
P6 | 180,330,835,515 | 192,048,880,421 |
全部的 | 180,998,504,099 | 192,774,759,297 |
因此,計數器不在線程之間共享。
這個結論與SMotherSpectre[^2]並不沖突,SMotherSpectre[^2]以時間作為旁道。 (例如,線程 2 在端口 1 上等待更長的時間才能使用端口 1。)
執行占用特定端口的指令並測量它們的時序可以推斷在同一端口上執行的其他指令。 我們首先選擇兩條指令,每條指令都安排在一個單獨的、不同的執行端口上。 一個線程運行並計時在端口 a 上調度的一長串單個 µop 指令,同時另一個線程運行在端口 b 上調度的一長串指令。 我們預計,如果 a = b,則發生爭用,並且與 a ≠ b 的情況相比,測量的執行時間更長。
[^1]:Abel、Andreas 和 Jan Reineke。 “最新英特爾微架構上基本塊的准確吞吐量預測。” arXiv 預印本 arXiv:2107.14210 (2021)。
[^2]:Bhattacharyya、Atri、Alexandra Sandulescu、Matthias Neugschwandtner、Alessandro Sorniotti、Babak Falsafi、Mathias Payer 和 Anil Kurmus。 “SMoTherSpectre:通過端口爭用利用投機執行。” 2019 年 ACM SIGSAC 計算機和通信安全會議論文集,2019 年 11 月 6 日,785-800。 https://doi.org/10.1145/3319535.3363194 。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.