我必須做一個大學專案,我們必須使用快取優化來提高給定代碼的性能,但我們不能使用編譯器優化來實作它。
我閱讀參考書目的想法之一是將基本塊的開頭與行快取大小對齊。但是你能做這樣的事情嗎:
asm(".align 64;")
for(int i = 0; i<N; i )
... (whole basic block)
為了實作我正在尋找的東西?我不知道是否可以在指令對齊方面做到這一點。我已經看到了一些類似_mm_malloc
實作資料對齊的技巧,但沒有任何指令。誰能給我一些關于此事的資訊?
uj5u.com熱心網友回復:
TL:DR: 這可能不是很有用(因為現代 x86 帶有 uop 快取通常不關心代碼對齊1),但在do{}while()
回圈前“作業” ,它可以直接編譯為具有相同布局的 asm,在回圈的實際頂部之前沒有任何回圈設定(序言)指令。(向后分支的目標)。
一般來說,https://gcc.gnu.org/wiki/DontUseInlineAsm尤其是永遠不要asm("foo");
在函式中使用 GNU C Basic,但在除錯模式下(-O0
默認,也就是禁用優化)每個陳述句(包括asm();
)編譯成一個單獨的塊asm 按源順序排列。因此,您的情況實際上并不需要 Extendedasm(".p2align 4" ::: "memory")
來訂購 asm 陳述句 wrt。記憶體操作。(同樣在最近的 GCC 中,對于具有非空模板字串的 Basic asm,記憶體破壞器是隱含的)。在最壞的情況下,啟用優化后,填充可能會變得無用并損害性能,但不會影響正確性,這與asm()
.
這實際上是如何編譯的
這并不完全有效;Cfor
回圈在asm 回圈之前編譯為一些 asm 指令。特別是for(a;b;c)
在陳述句中使用帶有一些首次迭代前初始化的回圈時a
!您當然可以在源代碼中將其提取出來,但 GCC 的編譯和回圈-O0
策略是在底部進入帶有 to 條件的回圈。while
for
jmp
但jmp
僅此一項只是一條小的(2 位元組)指令,因此在此之前對齊會將回圈頂部置于可能的指令獲取塊的開頭附近,如果這是一個瓶頸,它仍然可以獲得大部分好處。(或者在一組新的 uop-cache 行的開頭附近 Sandybridge-family x86 其中 32 位元組邊界是相關的。甚至是 64 位元組 I-cache 行,盡管這很少相關并且可能導致執行大量 NOP達到那個邊界。和臃腫的代碼大小。)
void foo(register int *p)
{
// always use .p2align n or .balign 1<<n so it's unambiguous across targets like MacOS vs. Linux, never .align
asm(" .p2align 5 # from inline asm");
for (register int *endp = p 102400; p<endp ; p ) {
*p = 123;
}
}
在Godbolt 編譯器資源管理器上編譯如下。請注意,我使用的方式register
意味著盡管進行了除錯構建,但我得到了不可怕的 asm,并且不必合并p
或p <= endp
減少*(p ) = 123;
存盤/重新加載開銷(因為對于register
本地人來說首先沒有任何開銷)。而且我使用了指標增量/比較來保持 asm 簡單,并且更難除錯模式去優化為更多浪費的 asm 指令。
# GCC11.3 -O0 (the default with no options, except for -masm=intel added by Godbolt)
foo:
push rbp
mov rbp, rsp
push rbx # GCC stupidly picks a call-preserved reg it has to save
mov rax, rdi
.p2align 5 # from inline asm
lea rbx, [rax 409600] # endp = p 102400
jmp .L2 # jump to the p<endp condition before the first iteration
## The actual top of the loop. 9 bytes past the alignment boundary
.L3: # do{
mov edx, DWORD PTR [rax]
add edx, 123
mov DWORD PTR [rax], edx # A memory destination add dword [rax], 123 would be 2 uops for the front-end (fused-domain) on Intel, vs. 3 for 3 separate instructions.
add rax, 4 # p
.L2:
cmp rax, rbx
jb .L3 # }while(p<endp)
nop
nop # These aren't for alignment, IDK what this is for.
mov rbx, QWORD PTR [rbp-8] # restore RBX
leave # and restore RBP / tear down stack frame
ret
這個回圈長 5 微秒(假設 cmp/JCC 的宏融合),因此如果一切順利,可以在 Ice Lake 或 Zen 上以每次迭代運行 1 個回圈。(每個周期加載/存盤 1 個 dword 的記憶體帶寬并不多,因此即使它不適合 L3 cahce,它也應該跟上一個大陣列。)或者在 Haswell 上,例如,每次迭代可能 1.25 個周期,或者由于回圈緩沖效應可能會更糟。
如果你在 Godbolt 上使用“二進制”輸出模式,你可以看到這lea rbx, [rax 409600]
是一個 7 位元組的指令,whilejmp .L2
是 2 個位元組,回圈頂部的地址是0x401149
,即 16 位元組的 fetch-block 中的 9 個位元組, 在獲取該大小的 CPU 上。我對齊了 32,所以它只浪費了與這個塊關聯的第一個 uop 快取行中的 2 個 uop,所以我們在 32 位元組塊方面仍然相對較好。
(Godbolt“二進制”模式編譯并鏈接成可執行檔案,并在objdump -d
其上運行。這也讓我們看到.p2align
指令擴展為某個寬度的 NOP 指令,或者如果它必須跳過超過 11 個位元組,則擴展為一個以上,默認x86-64 的 GAS 的最大 NOP 寬度。請記住,每次控制通過此 asm 陳述句時,必須獲取這些 NOP 指令并通過管道,因此函式內部的大量對齊對它和 I- 來說都是一件壞事快取足跡。)
一個相當明顯的轉換在.p2align
. (如果您好奇,請參閱所有這些源版本的 Godbolt 鏈接中的 asm)。
register int *endp = p 102400;
asm(" .p2align 5 # from inline asm");
for ( ; p < endp ; p ) {
*p = 123;
}
或者while (p < endp){... ; p }
也可以解決問題。asm 回圈的頂部變為以下內容,回圈條件只有 2 個位元組jmp
。所以這是相當不錯的,并且獲得了大部分好處。
lea rbx, [rax 409600]
.p2align 5 # from inline asm
jmp .L5 # 2-byte instruction
.L6:
或許可以用for(foo=bar, asm(".p2align 4) ; p<endp ; p )
. 但是,如果您在陳述句的第一部分宣告一個變數for
,逗號運算子將無法讓您潛入單獨的陳述句。
要真正對齊 asm 回圈,我們可以將其寫為do{}while
.
register int *endp = p 102400;
asm(" .p2align 5 # from inline asm");
do {
*p = 123;
p ;
}while(p < endp);
lea rbx, [rax 409600]
.p2align 5 # from inline asm
.L8: # do{
mov edx, DWORD PTR [rax]
add edx, 123
mov DWORD PTR [rax], edx
add rax, 4
cmp rax, rbx
jb .L8 # while(p<endp)
一開始沒有jmp
,回圈內沒有分支目標標簽。(如果您想嘗試-falign-labels=32
讓 GCC 為您填充而不將 NOP放入回圈中,這很有趣。見下文:-falign-loops 不起作用-O0
。)
由于我對非零大小進行硬編碼,因此p == endp
在第一次迭代之前不會運行任何檢查。如果該長度是運行時變數,例如函式 arg,則可以if(n==0) return;
在回圈之前執行。或者更一般地說,如果不能證明它總是運行至少一次迭代,則在編譯啟用優化的或回圈時將回圈放在GCC 中。if
for
while
if(n!=0) {
register int *endp = p n;
asm (".p2align 4");
do {
...
}while(p!=endp);
}
讓 GCC 為您執行此操作:-falign-loops=16
不起作用-O0
GCC-O2
啟用-falign-loops=16:11:8
或類似的東西(如果跳過少于 11 個位元組,則按 16 對齊,否則按 8 對齊)。這就是為什么 GCC 使用兩個.p2align
指令的序列,第一個有填充限制(參見 GAS 手冊)。
.p2align 4,,10 # what GCC does on its own
.p2align 3
但是 using-falign-loops=16
在-O0
. 似乎 GCC -O0 不知道什么是回圈。:P
但是,GCC甚至-falign-labels
在-O0
. 但不幸的是,這適用于所有標簽,包括內部回圈內的回圈入口點。 神箭。
# gcc -O0 -falign-labels=16
## from compiling endp=...; asm(); while() {}
lea rbx, [rax 409600] # endp = ...
.p2align 5 # from inline asm
jmp .L5
.p2align 4 # from GCC itself, pads another 14 bytes to an odd multiple of 16 (if you didn't remove the manual .p2align 5)
.L6:
mov edx, DWORD PTR [rax]
add edx, 123
mov DWORD PTR [rax], edx
add rax, 4
.p2align 4 # from GCC itself: one 5-byte NOP in this particular case
.L5:
cmp rax, rbx
jb .L6
將 NOP 放在最內層回圈中比在現代 x86 CPU 上錯開它的開始更糟糕。
回圈沒有這個問題do{}while()
,但在這種情況下,它似乎也可以用來asm()
在那里放置對齊指令。
(我使用了如何從 GCC/clang 程式集輸出中洗掉“噪音”?編譯選項以在不過濾掉指令的情況下最大限度地減少混亂,其中包括.p2align
. 如果我只是想看看行內匯編的去向,我可以asm("nop #hi mom")
使用它在過濾掉指令后可見。)
如果您可以使用行內匯編,但必須使用反優化除錯模式進行編譯,那么在具有輸入/輸出約束的行內匯編中重寫整個內部回圈可能會大大加快速度。(但不要真的這樣做;很難做到正確,在現實生活中,普通人只會啟用優化作為第一步。)
腳注 1:代碼對齊對現代 x86 幫助不大,可能對其他人有幫助
即使您確實對齊了向后分支的目標(而不僅僅是一些回圈序言),這也不太可能有幫助;現代 x86 CPU 帶有 uop 快取(Sandybridge 系列和 Zen 系列)和回圈緩沖區(Nehalem 和后來的 Intel)不太關心回圈對齊。
它可以在較舊的 x86 CPU 或其他一些 ISA 上提供更多幫助;只有 x86 很難解碼,以至于 uop 快取是一回事 (您實際上并沒有指定 x86,但目前大多數人在他們的臺式機/筆記本電腦中使用 x86 CPU,所以我假設。)
分支目標對齊有幫助(尤其是回圈頂部)的主要原因是,當 CPU 獲取包含目標地址的 16 位元組對齊塊時,該塊中的大部分機器代碼將在它之后,因此是即將運行另一個迭代的回圈體。(分支目標之前的位元組在該提取周期中被浪費)。
但是最壞的未對齊情況(除非有其他奇怪的影響)只會花費您 1 個額外的前端提取周期來獲得回圈體中的更多指令。(例如,如果回圈的頂部有一個以 結尾的地址0xf
,那么它是 16 位元組塊的最后一個位元組,則包含該位元組的對齊 16 位元組塊將只包含最后一個有用的位元組。)這可能是一個單位元組指令cdq
,但管道通常是 4 條指令寬,或更多。
(或者在早期的英特爾 P6 系列時代,在提取、預解碼(長度查找)和解碼之間存在緩沖區之前,為 3 寬。如果回圈的其余部分有效解碼并且平均指令長度很短,則緩沖可以隱藏氣泡. 但是解碼仍然是一個重要的瓶頸,直到 Nehalem 的回圈緩沖區可以為一個小回圈(幾十個 uops)回收解碼結果(uops)。Sandybridge 系列添加了一個 uop 快取來快取包含多個被呼叫的函式的大回圈David Kanter 對SnB的深入研究有很好的框圖,另見https://www.agner.org/optimize/特別是 Agner 的 microarch pdf。
即使這樣,它也只有在前端(指令獲取/解碼)帶寬是一個問題時才有幫助,而不是一些后端瓶頸(實際執行這些指令)。亂序 exec 通常可以很好地讓 CPU 運行得與最慢的瓶頸一樣快,而不是等到快取未命中加載之后才能獲取和解碼以后的指令。(請參閱此,此,尤其是現代微處理器 90 分鐘指南!。)
在某些情況下,它可以在微碼更新禁用回圈緩沖區 (LSD) 的 Skylake CPU 上有所幫助,因此跨 32 位元組邊界拆分的微小回圈體最多可以每 2 個周期運行 1 次迭代(從 2 個單獨的快取行)。或者再次在 Skylake 上,以這種方式調整代碼對齊可以幫助避免 JCC 勘誤(這可以使您的部分代碼從舊版解碼而不是 uop 快取運行),如果您無法通過-Wa,-mbranches-within-32B-boundaries
讓匯編程式解決它。(如何減輕英特爾 jcc 勘誤表對 gcc 的影響?)。這些問題特定于 Skylake 衍生的微架構,并在 Ice Lake 中得到修復。
當然,反優化除錯模式代碼非常臃腫,以至于即使是緊密回圈也不太可能少于 8 uop,因此 32 位元組邊界問題可能不會造成太大影響。但是,如果您設法通過register
在本地變數上使用來避免存盤/重新加載延遲瓶頸(是的,這僅在除錯構建中起作用,否則毫無意義1),通過管道獲取所有這些低效指令的前端瓶頸很可能會受到影響在 Skylake CPU 上,如果由于回圈內部或回圈底部的條件分支結束而導致內部回圈最終超出 JCC 勘誤表。
無論如何,正如 Eric 評論的那樣,您的任務可能更多是關于資料訪問模式,可能還有 layout 和 alignment。大概涉及在一些大量記憶體上的小回圈,因為 L2 或 L3 快取未命中是唯一比禁用優化構建時慢到足以成為瓶頸的事情。在某些情況下可能是 L1d,如果您設法讓編譯器為除錯模式生成非可怕的 asm,或者如果負載使用延遲(不僅僅是吞吐量)是關鍵路徑的一部分。
腳注2:-O0
很愚蠢,但register int i
可以提供幫助
請參閱 C 回圈優化幫助以了解最終分配(禁用編譯器優化)回復:為除錯模式優化源代碼是多么愚蠢,或者為正常用例以這種方式進行基準測驗。但也提到了一些在這種情況下更快的事情(與正常構建不同),例如在單個陳述句或運算式中執行更多操作,因為編譯器不會跨陳述句將內容保存在暫存器中。
(另請參閱為什么 clang 會使用 -O0 產生低效的 asm(對于這個簡單的浮點求和)?有關詳細資訊)
register
變數除外;那個過時的關鍵字仍然可以為使用 GCC 的未優化構建做一些事情(但不是 clang)。在最近的 C 版本中,它已被正式棄用甚至洗掉,但目前還沒有 C。
您肯定希望使用register int i
讓除錯版本將其保存在暫存器中,并像手寫 asm 一樣撰寫您的 C。例如,arr[i]
在適當的地方使用指標增量代替,尤其是對于沒有索引尋址模式的 ISA。
register
變數在你的內部回圈中是最重要的,并且在禁用優化的情況下,編譯器可能不太聰明地決定哪個register
var 在用完時實際獲取暫存器。(x86-64 除了堆疊指標之外還有 15 個整數 reg,除錯版本會將其中一個用于幀指標。)
尤其是對于在回圈內發生變化的變數,以避免存盤/重新加載延遲瓶頸,例如for(register int i=1000000 ; --i ; );
每個時鐘可能運行 1 次迭代,而register
在 Skylake 等現代 x86-64 CPU 上不運行 5 或 6 次。
如果使用整數變數作為陣列索引,請將其設為intptr_t
或uintptr_t
( #include <stdint.h>
)(如果可能),因此編譯器不必將符號擴展從 32 位int
重做為 64 位指標寬度以用于尋址模式。
(除非您正在為 AArch64 進行編譯,它具有采用 64 位暫存器和 32 位暫存器的尋址模式,進行符號或零擴展并忽略窄整數 reg 中的高垃圾。正是因為這是編譯器可以做到的。 t 總是優化掉。雖然它們通常可以歸功于有符號整數溢位是未定義的行為,允許編譯器擴大整數回圈變數或轉換為指標增量。)
也松散相關:為英特爾 Sandybridge 系列 CPU 中的管道取消優化程式有一節是關于通過快取效果故意使事情變慢,所以反其道而行之。可能不是很適用,IDK你的問題是什么樣的。
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/465499.html
上一篇:BPF驗證器以“Permissiondenied(13)!”拒絕。使用bpf_trace_printk()時
下一篇:Spring檔案集成:使用FileWritingMessageHandler寫入檔案時出現錯誤FileSystemException