這是我開始使用這個網站 10 年后的第一個問題。大多數經典問題已經被問及并得到了回答,所以直到今天我才覺得有必要創建一個帳戶。因此,首先感謝您分享知識并讓我們的生活更輕松。
大家好,因為顯然我不能在訊息的開頭打招呼。
我的問題
我有 2個大小相同的相鄰位元組緩沖區(每個緩沖區大約 20 MB)。我只想計算它們之間的差異。
我的問題
在配備 3600MT RAM 的 4.8GHz Intel I7 9700K 上運行此回圈需要多長時間?我們如何計算最大理論速度?
我試過的
uint64_t compareFunction(const char *const __restrict buffer, const uint64_t commonSize)
{
uint64_t diffFound = 0;
for(uint64_t byte = 0; byte < commonSize; byte)
diffFound = static_cast<uint64_t>(buffer[byte] != buffer[byte commonSize]);
return diffFound;
}
在我的 PC 上需要 11 毫秒(9700K 4.8Ghz RAM 3600 Windows 10 Clang 14.0.6 -O3 MinGW),我覺得它太慢了,我錯過了一些東西。
在 CPU 上讀取 40MB 的時間應該少于 2ms(我的 RAM 帶寬在 20 到 30GB/s 之間)
我不知道如何計算執行一次迭代所需的周期(特別是因為現在 CPU 是超標量)所以如果我假設每個操作 1 個周期并且如果我不搞亂我的計數,那么每次迭代應該是 10 個操作-> 2 億次操作 -> 4.8 Ghz,只有一個執行單元 -> 40 毫秒,所以顯然我在如何計算每個回圈的周期數上是錯誤的。
有趣的事實是我在 linux PopOS GCC 11.2 -O3 上嘗試過,它的運行時間為 4.5 毫秒。為什么會有這樣的差異?
這是由clang產生的向量化和標量的反匯編。我正在嘗試添加 GCC,但 SO 一直在哭。
Clang14 O1
Clang14 O3
uj5u.com熱心網友回復:
TLDR:Clang 代碼如此緩慢的原因來自于糟糕的矢量化方法使埠 5 飽和(已知這通常是一個問題)。GCC 在這里做得更好,但它仍然遠未達到高效。可以使用 AVX-2 撰寫更快的基于塊的代碼,而不會使埠 5 飽和。
非向量化Clang代碼分析
要了解發生了什么,最好從一個簡單的示例開始。確實,正如您所說,現代處理器是超標量的,因此在這種架構上理解某些生成代碼的速度并不容易。
Clang 使用-O1優化標志生成的代碼是一個好的開始。這是您的問題中提供的 GodBold 生成的熱回圈代碼:
(instructions) (ports)
.LBB0_4:
movzx r9d, byte ptr [rdi rdx] p23
xor ecx, ecx p0156
cmp r9b, byte ptr [r8 rdx] p0156 p23
setne cl p06
add rax, rcx p0156
add rdx, 1 p0156
mov rcx, rsi (optimized)
add rcx, rdx p0156
jne .LBB0_4 p06
像 Coffee Lake 9700K 這樣的現代處理器分為兩大部分:前端獲取/解碼指令(并將它們拆分為微指令,又名uops),以及后端調度/執行它們。后端在許多埠上調度微指令,它們中的每一個都可以執行一些特定的指令集(例如,僅記憶體加載,或僅算術指令)。對于每條指令,我都放置了可以執行它們的埠。p0156 p23意味著指令被分成兩個微指令:第一個可以由埠 0 或 1 或 5 或 6 執行,第二個可以由埠 2 或 3 執行。請注意,前端可以以某種方式優化代碼,因此不要為基本指令產生任何微指令,比如mov在回圈中(感謝一種稱為暫存器重命名的機制)。
對于每個回圈迭代,處理器需要從記憶體中讀取 2 個值。像 9700K 這樣的 Coffee Lake 處理器每個周期可以加載兩個值,因此回圈至少需要 1 個周期/迭代(假設加載r9d并且r9b不會由于使用相同的不同部分而發生沖突r964 位暫存器)。這個處理器有一個微指令快取,回圈有很多指令,所以解碼部分應該不是問題。話雖如此,要執行 9 個微指令,處理器每個周期只能執行 6 個微指令,因此回圈不能少于 1.5 個周期/迭代。更準確地說,埠 0、1、5 和 6 處于壓力之下,因此即使假設處理器完美地平衡了微指令的負載,也需要 2 個周期/迭代。這是一個樂觀的下限執行時間,因為處理器可能無法完美地調度指令,并且有很多事情可能會出錯(比如我沒有看到的偷偷摸摸的隱藏依賴項)。在 4.8GHz 頻率下,最終執行時間至少為 8.3 ms。3個回圈/迭代可以達到12.5 ms(注意2.
可以使用unrolling改進回圈。實際上,只需要執行回圈而不是實際計算就需要大量指令。展開可以幫助增加有用指令的比率,從而更好地使用可用埠。盡管如此,這 2 個負載阻止回圈快于 1 個周期/迭代,即 4.2 ms。
向量化 Clang 代碼分析
Clang 生成的向量化代碼很復雜。可以嘗試應用與之前代碼相同的分析,但這將是一項乏味的任務。
可以注意到,即使代碼是矢量化的,負載也不是矢量化的。這是一個問題,因為每個周期只能完成 2 次加載。話雖如此,加載是由兩個連續的 char 值對執行的,因此與之前生成的代碼相比,加載并沒有那么慢。
Clang 這樣做是因為只有兩個 64 位值可以容納在 128 位 SSE 暫存器和一個 64 位中,并且它需要這樣做,因為diffFound它是一個 64 位整數。8位到64位的轉換是代碼中最大的問題因為它需要幾個 SSE 指令來進行轉換。此外,由于 Coffee Lake 上有 3 個 SSE 整數單元,并且每個單元一次只能計算兩個 64 位整數,因此一次只能計算 4 個整數。最后,Clang 只在每個 SSE 暫存器中放了 2 個值(并使用其中 4 個值來計算每次回圈迭代的 8 個專案),因此人們應該期望代碼運行速度快兩倍以上(尤其是由于 SSE 和回圈展開),但由于 SSE 埠比 ALU 埠少,而且型別轉換所需的指令更多,因此這種情況并不多。簡而言之,向量化顯然是低效的,但是在這種情況下,Clang 生成高效的代碼并不是那么容易。盡管如此,使用 28 個 SSE 指令和 3 個 SSE 整數單元,每個回圈計算 8 個專案,人們應該期望代碼的計算部分占用的28/3/8 ~= 1.2周期/專案與您可以觀察到的相差甚遠(這不是由于其他指令,因為它們大多可以并行執行,因為它們大多可以安排在其他埠上)。
事實上,性能問題肯定來自埠 5 的飽和。事實上,這個埠是唯一可以洗牌 SIMD 暫存器項的埠。因此,指令punpcklbw、甚至只能在埠 5 上執行。這是 SIMD 代碼的一個非常常見的問題。這是一個大問題,因為每個回圈有 20 條指令,處理器甚至可能無法完美地使用它。這意味著代碼至少需要 10.4 毫秒,這非常接近觀察到的執行時間(11 毫秒)。pshuflwpshufdmovd
矢量化GCC代碼分析
與 Clang 相比,GCC 生成的代碼實際上相當不錯。首先,GCC 直接使用 SIMD 指令加載專案,效率更高,因為每條指令(并且通過迭代)計算 16 個專案:每次迭代只需要 2 個加載微指令,減少埠 2 和 3 的壓力(1 個周期/迭代那,所以 0.0625 周期/專案)。其次,GCC 只使用 14punpckhwd條指令,而每次迭代計算 16 項,減少了埠 5 的臨界壓力(0.875 周期/項)。第三,SIMD 暫存器幾乎被完全使用,至少用于比較,因為pcmpeqb比較指令一次比較 16 個專案(而 Clang 是 2 個)。其他指令paddq很便宜(例如,paddq可以安排在 3 個 SSE 埠上),它們應該不會對執行時間產生太大影響。最后,這個版本應該還是以埠 5 為界,但是應該比 Clang 版本快很多。實際上,應該期望執行時間達到 1 個周期/專案(因為埠調度肯定不是完美的,記憶體負載可能會引入一些停滯周期)。這意味著執行時間為 4.2 ms。這接近觀察到的結果。
更快的實施
GCC 的實作并不完美。
首先,它不使用處理器支持的 AVX2,因為沒有提供標志(或任何類似的-mavx2-march=native標志,如)。事實上,GCC 和其他主流編譯器一樣,默認情況下只使用 SSE2 是為了與以前的架構兼容:SSE2 可以在所有 x86-64 處理器上安全使用,但不能在 SSE3、SSSE3、SSE4.1、SSE4.2 等其他指令集上安全使用, AVX,AVX2。有了這樣的標志,GCC 應該能夠生成記憶體系結代碼。
此外,編譯器理論上可以執行多級求和。這個想法是使用大小為 1024 項(即 64x16 項)的塊在 8 位寬的 SIMD 通道中累積比較結果。這是安全的,因為每個通道的值不能超過 64。為避免溢位,需要將累加值存盤在更寬的 SIMD 通道中(例如 64 位通道)。使用這種策略,punpckhwd指令的開銷減少了 64 倍。這是一個很大的改進,因為它消除了埠 5 的飽和。即使只使用 SSE2,這種策略也應該足以生成記憶體系結代碼。這是一個需要標志有效的未經測驗的代碼示例。-fopenmp-simd
uint64_t compareFunction(const char *const __restrict buffer, const uint64_t commonSize)
{
uint64_t byteChunk = 0;
uint64_t diffFound = 0;
if(commonSize >= 127)
{
for(; byteChunk < commonSize-127; byteChunk = 128)
{
uint8_t tmpDiffFound = 0;
#pragma omp simd reduction( :tmpDiffFound)
for(uint64_t byte = byteChunk; byte < byteChunk 128; byte)
tmpDiffFound = buffer[byte] != buffer[byte commonSize];
diffFound = tmpDiffFound;
}
}
for(uint64_t byte = byteChunk; byte < commonSize; byte)
diffFound = buffer[byte] != buffer[byte commonSize];
return diffFound;
}
GCC和Clang都生成了相當高效的代碼(雖然對于快取中的資料而言不是最佳的),尤其是 Clang。以下是 Clang 使用 AVX2 生成的代碼示例:
.LBB0_4:
lea r10, [rdx 128]
vmovdqu ymm2, ymmword ptr [r9 rdx - 96]
vmovdqu ymm3, ymmword ptr [r9 rdx - 64]
vmovdqu ymm4, ymmword ptr [r9 rdx - 32]
vpcmpeqb ymm2, ymm2, ymmword ptr [rcx rdx - 96]
vpcmpeqb ymm3, ymm3, ymmword ptr [rcx rdx - 64]
vpcmpeqb ymm4, ymm4, ymmword ptr [rcx rdx - 32]
vmovdqu ymm5, ymmword ptr [r9 rdx]
vpaddb ymm2, ymm4, ymm2
vpcmpeqb ymm4, ymm5, ymmword ptr [rcx rdx]
vpaddb ymm3, ymm4, ymm3
vpaddb ymm2, ymm3, ymm2
vpaddb ymm2, ymm2, ymm0
vextracti128 xmm3, ymm2, 1
vpaddb xmm2, xmm2, xmm3
vpshufd xmm3, xmm2, 238
vpaddb xmm2, xmm2, xmm3
vpsadbw xmm2, xmm2, xmm1
vpextrb edx, xmm2, 0
add rax, rdx
mov rdx, r10
cmp r10, r8
jb .LBB0_4
所有負載都是 256 位 SIMD 的。數量vpcmpeqb是最優的。數量vpaddb比較好。其他指令很少,但它們顯然不應該成為瓶頸。該回圈每次迭代對 128 個專案進行操作,我希望每次迭代對于已經在快取中的資料花費不到十幾個周期(否則它應該完全受記憶體限制)。這意味著 <0.1 個周期/項,即遠低于之前的實作。事實上,uiCA 工具顯示大約 0.055 個周期/項,即 81 GiB/s!可以使用 SIMD 內在函式手動撰寫更好的代碼,但代價是可移植性、維護性和可讀性明顯變差。
請注意,生成順序記憶體限制并不總是意味著 RAM 吞吐量會飽和。事實上,在一個內核上,有時沒有足夠的并發性來隱藏記憶體操作的延遲,盡管它在您的處理器上應該沒問題(就像在我的 i5-9600KF 上,帶有 2 個交錯的 3200 MHz DDR4 記憶體通道)。
uj5u.com熱心網友回復:
是的,如果您的資料在快取中不熱,即使 SSE2 也應該跟上記憶體帶寬。如果資料在 L1d 高速快取中很熱,或者任何外部高速快取級別可以提供的帶寬,那么每個周期(來自兩個 32 位元組加載)的比較和總和是完全可能的。
如果不是,編譯器做得不好。不幸的是,對于像這樣減少到更廣泛的變數的問題,這很常見;編譯器不知道對位元組求和的良好矢量化策略,尤其是必須為 0/-1 的比較結果位元組。它們可能會立即擴展到 64 位pmovsxbq(如果 SSE4.1 指令不可用,甚至更糟)。
所以即使-O3 -march=native沒有太大幫助;這是一個很大的優化失誤;希望 GCC 和 clang 能夠在某個時候學習如何對這種回圈進行矢量化,總結比較結果可能會出現在足夠多的代碼庫中,值得識別這種模式。
有效的方法是使用psadbw水平求和到 qwords。但只有在內部回圈之后,才會對 進行一些迭代vsum -= cmp(p, q),減去 0 或 -1 以增加或不增加計數器。8 位元素可以進行 255 次迭代而沒有溢位的風險。通過展開多個向量累加器,每個向量都有 32 個位元組,因此您不必經常跳出該內部回圈。
有關手動矢量化 AVX2 代碼,請參閱如何使用 SIMD 計算字符出現次數。 (并且一個答案有一個指向 SSE2 版本的 Godbolt 鏈接。)對比較結果求和與此問題相同,但是您正在加載兩個向量來饋送 pcmpeqb,而不是在回圈外廣播一個位元組以查找單個字符的出現.
那里的答案是在 i7-6700 Skylake 上報告 AVX2 為 28 GB/s,SSE2 為 23 GB/s(只有 3.4GHz,也許他們禁用了 turbo 或者只是報告了額定速度。DRAM 速度沒有提到。 )
我希望 2 個輸入資料流能夠實作與一個資料流大致相同的持續帶寬。
如果您對適合 L2 快取的較小陣列的重復傳遞進行基準測驗,則優化這一點會更有趣,那么 ALU 指令的效率很重要。(該問題的答案中的策略非常好,并且針對這種情況進行了很好的調整。)
快速計算兩個陣列之間相等位元組的數量是一個較舊的問答,使用更糟糕的策略,而不是使用psadbw將位元組求和為 64 位。(但不如 GCC/clang 差,但在擴展至 32 位時仍然會出現 hsumming。)
多執行緒/內核對現代桌面幾乎沒有幫助,尤其是在像您這樣的高內核時鐘下。記憶體延遲足夠低,每個內核都有足夠的緩沖區來保持足夠多的請求在傳輸中,它幾乎可以使雙通道 DRAM 控制器飽和。
在大型 Xeon 上,情況會大不相同。您需要大多數內核來實作峰值聚合帶寬,即使對于 memcpy 或 memset 也是如此,因此 ALU 作業為零,只需加載/存盤。更高的延遲意味著單個內核的可用記憶體帶寬比臺式機上的少得多(即使在絕對意義上,更不用說占 6 個通道而不是 2 個通道的百分比)。另請參閱針對 memcpy 的增強型 REP MOVSB以及為什么 Skylake 在單執行緒記憶體吞吐量方面比 Broadwell-E 好得多?
可編譯為不太糟糕的 asm 的便攜式源代碼,從 Jér?me 進行了微優化:假設 L1d 快取命中,每 4 個 32 位元組向量從 7 或 8 個周期減少到 5.5 個周期。
仍然不好(因為它減少到每 128 個位元組的標量,或者如果你想嘗試的話,可以減少到 192 個),但是@Jér?me Richard 想出了一個聰明的方法來給 clang 一些東西,它可以用一個好的策略來矢量化一個短,用一個uint8_t sum,使用它作為足夠短的內部回圈不會溢位。
但正如我們在他的回答中看到的那樣,clang 仍然用這個回圈做了一些愚蠢的事情。我修改了回圈控制以使用指標增量,這減少了一點回圈開銷,只有一個指標添加和比較/jcc,而不是 LEA/MOV。我不知道為什么 clang 使用整數索引效率低下。
它避免了vpcmpeqb記憶體源運算元的索引尋址模式,讓它們在 Intel CPU 上保持微融合。(Clang 似乎根本不知道這很重要!!=在源代碼中反轉運算元足以使其使用索引尋址模式vpcmpeqb而不是vmovdqu純負載。)
// micro-optimized version of Jér?me's function, clang compiles this better
// instead of 2 arrays, it compares first and 2nd half of one array, which lets it index one relative to the other with an offset if we hand-hold clang into doing that.
uint64_t compareFunction_sink_fixup(const char *const __restrict buffer, const size_t commonSize)
{
uint64_t byteChunk = 0;
uint64_t diffFound = 0;
const char *endp = buffer commonSize;
const char *__restrict ptr = buffer;
if(commonSize >= 127) {
// A signed type for commonSize wouldn't avoid UB in pointer subtraction creating a pointer before the object
// in practice it would be fine except maybe when inlining into a function where the compiler could see a compile-time-constant array size.
for(; ptr < endp-127 ; ptr = 128)
{
uint8_t tmpDiffFound = 0;
#pragma omp simd reduction( :tmpDiffFound)
for(int off = 0 ; off < 128; off)
tmpDiffFound = ptr[off commonSize] != ptr[off];
// without AVX-512, we get -1 for ==, 0 for not-equal. So clang adds set1_epi(4) to each bucket that holds the sum of four 0 / -1 elements
diffFound = tmpDiffFound;
}
}
// clang still auto-vectorizes, but knows the max trip count is only 127
// so doesn't unroll, just 4 bytes per iter.
for(int byte = 0 ; byte < commonSize % 128 ; byte)
diffFound = ptr[byte] != ptr[byte commonSize];
return diffFound;
}
帶有clang15的 Godbolt-O3 -fopenmp-simd -mavx2 -march=skylake -mbranches-within-32B-boundaries
# The main loop, from clang 15 for x86-64 Skylake
.LBB0_4: # =>This Inner Loop Header: Depth=1
vmovdqu ymm2, ymmword ptr [rdi rsi]
vmovdqu ymm3, ymmword ptr [rdi rsi 32] # Indexed addressing modes are fine here
vmovdqu ymm4, ymmword ptr [rdi rsi 64]
vmovdqu ymm5, ymmword ptr [rdi rsi 96]
vpcmpeqb ymm2, ymm2, ymmword ptr [rdi] # non-indexed allow micro-fusion without un-lamination
vpcmpeqb ymm3, ymm3, ymmword ptr [rdi 32]
vpcmpeqb ymm4, ymm4, ymmword ptr [rdi 64]
vpaddb ymm2, ymm4, ymm2
vpcmpeqb ymm4, ymm5, ymmword ptr [rdi 96]
vpaddb ymm3, ymm4, ymm3
vpaddb ymm2, ymm2, ymm3
vpaddb ymm2, ymm2, ymm0 # add a vector of set1_epi8(4) to turn sums of 0 / -1 into sums of 1 / 0
vextracti128 xmm3, ymm2, 1
vpaddb xmm2, xmm2, xmm3
vpshufd xmm3, xmm2, 238 # xmm3 = xmm2[2,3,2,3]
vpaddb xmm2, xmm2, xmm3 # reduced to 8 bytes
vpsadbw xmm2, xmm2, xmm1 # hsum to one qword
vpextrb edx, xmm2, 0 # extract and zero-extend
add rax, rdx # accumulate the chunk sum
sub rdi, -128 # pointer increment (with a sign_extended_imm8 instead of imm32)
cmp rdi, rcx
jb .LBB0_4 # }while(p < endp)
這可以使用192而不是128進一步攤銷回圈開銷,代價是需要做2(不是 2 的冪),并使清理回圈最壞情況為 191 個位元組。我們不能達到 256 或任何高于 UINT8_MAX (255) 的值,并且必須堅持 32 的倍數。或 64 為好衡量標準。
還有一個額外vpaddb的修正常數 ,set1_epi8(4)它將四個 0 / -1 的總和轉換為來自 C 運算子的四個 1 / 0 結果的總和!=。
我認為沒有任何方法可以擺脫它或將其從回圈中洗掉,同時仍然累積到 auint8_t中,這對于 clang 以這種方式進行矢量化是必要的。它不知道如何使用vpsadbw來擴大(非截斷)位元組總和,這具有諷刺意味,因為這是它在針對全零暫存器使用時實際所做的。如果你做類似的事情,sum = ptr[off commonSize] == ptr[off] ? -1 : 0你可以讓它vpcmpeqb直接使用結果,將 4 個向量與 3 個加法相加,并最終vpsadbw在一些縮減步驟后將其輸入。matches * 0xFF因此,對于每個 128 位元組的塊,您會得到一個截斷為 uint8_t的總和。或者作為int8_t,這是 的總和-1 * matches,所以 0..-128,它不會溢位有符號位元組。所以這很有趣。但是將零擴展添加到 64 位計數器可能會破壞資訊,并且外回圈內的符號擴展將花費另一條指令。這將是一個標量movsx指令而不是vpaddb,但這對 Skylake 來說并不重要,可能只有在使用 AVX-512 和 512 位向量的情況下(clang 和 GCC 都做得不好,不使用掩碼添加)。我們可以128*n_chunks - count在回圈之后從匹配總和中恢復差異嗎?不,我不這么認為。
uiCA 靜態分析預測,如果 L1d 快取中的資料很熱,Skylake(例如您的 CPU)將以5.51 個周期/迭代(4 個向量)運行主回圈,或者在 Ice Lake / Rocket Lake 上以 5.05 個運行主回圈。(我不得不手動調整 asm 以模擬填充效果-mbranches-within-32B-boundaries,因為 uiCA 的默認假設是回圈頂部相對于 32 位元組對齊邊界的位置。我本可以在 uiCA 中更改該設定。: /)
在實作這個次優策略時唯一錯過的微優化是它使用vpextrb(因為它不能證明不需要截斷 to uint8_t?)而不是vmovdor vmovq。因此,前端和后端的埠 5 需要額外的 uop。通過優化(鏈接中的注釋 取消注釋),S??kylake 上的 5.25c / iter 或 Ice Lake 上的 4.81,非常接近 2 負載/時鐘瓶頸。
(每迭代 6 個向量,192 位元組,預測 SKL 上每迭代 7 個周期,或每向量 1.166,低于每向量 5.5 / iter = 1.375。或 ICL/RKL = 1.08 c/vec 上約 6.5,命中后端ALU 埠瓶頸。)
這對于我們能夠哄騙從便攜式 C 源生成的東西來說還不錯,而每 4 個 32 位元組的向量比較需要 4 個周期,以實作高效的手動向量化。這很可能會跟上記憶體或快取帶寬,甚至來自 L2 快取,因此它非常有用,并且對于 L1d 中的熱資料不會慢很多。多處理一些微指令確實會損害無序執行,并且會占用更多共享物理內核的邏輯內核可以使用的執行資源。(超執行緒)。
不幸的是,gcc/clang沒有很好地利用 AVX-512。 如果您使用 512 位向量(或 256 位向量上的 AVX-512 功能),您將比較掩碼暫存器,然后執行vpaddb zmm0{k1}, zmm0, zmm1合并掩碼之類的操作以有條件地遞增向量,其中 zmm1 = set1_epi8( 1 ). (或-1帶有 . 的常數sub。)如果處理得當,每個向量的指令和 uop 計數應該與 AVX2 大致相同,但 gcc/clang 使用的數量大約是其兩倍,因此唯一的節省是減少到標量,這似乎是價格為了得到任何可用的東西。
這個版本還避免了清理回圈的展開,只是用每個迭代策略的啞 4 位元組向量化,這對于清理size8位元組來說是正確的。它同時使用vpxor翻轉和vpand將 0xff 轉換為 0x01 是非常愚蠢的,而它本來可以vpandn在一條指令中完成這兩件事。這將使清理回圈降低到 8 uop,只是 Haswell / Skylake 上管道寬度的兩倍,因此它將更有效地從回圈緩沖區發出,除了 Skylake 在微碼更新中禁用了它。這對 Haswell 會有所幫助
uj5u.com熱心網友回復:
如果我錯了,請糾正我,但答案似乎是
- -march=native 為勝利。
- 代碼的標量版本是 CPU 瓶頸而不是 RAM 瓶頸
- 使用 uica.uops.info 估計每個回圈的周期
我將嘗試撰寫自己的 AVX 代碼進行比較。
細節
經過一個下午的修補建議,這是我發現的鏗鏘聲:
-O1 大約 10 毫秒,標量代碼
-O3 啟用 SSE2 并且與 O1 一樣慢,可能較差的匯編代碼
-O3 -march=westmere 也啟用 SSE2 但更快(7 毫秒)
-O3 -march=native 啟用 AVX -> 2.5 毫秒和我們可能受到 RAM 帶寬的限制(接近理論速度)
標量 10ms 現在很有意義,因為根據那個很棒的工具 uica.uops.info 它需要
- 每個回圈 2.35 個回圈
- 整個比較的 4700 萬個周期(2000 萬次迭代)
- 處理器的時鐘頻率為 4.8GHz,這意味著它應該需要大約 9.8ms 并且接近測量值。
當沒有添加標志時,g 似乎會生成更好的默認代碼
- O1 11ms
- O2 標量仍然是 9ms
- O3 SSE 4.5ms
- O3 -march=westmere 7ms like clang
- O3 -march=native 3.4ms,比clang稍慢
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/533222.html
