對不起,我從來沒有理解過這里的規則。我已經洗掉了我所發的所有重復的帖子。這是第一個相關的問題。 請不要把這個帖子標記為與我的另一個帖子(將執行次數減少3倍,但執行效率幾乎沒有變化。在C語言中),即使代碼有些相似,它們提出了非常不同的問題。這也是我在同一天發現的兩個問題。一個類似的帖子已經被 "誤判 "重復了,然后被關閉。可能是我沒有弄清楚這個問題。我真的希望能得到答案,所以我重新發了帖子。希望大家都能看清楚這個問題,非常感謝!
在下面的C語言代碼中,我們可以看到 "誤判"。在下面的C代碼中,我在第一個測驗時間的回圈中加入了一個 "if "陳述句,執行時間完全相同。從理論上講,它應該更慢。盡管分支預測可以使其性能幾乎相同,但實際上卻變得快了很多。這其中的原理是什么?我嘗試使用clang和gcc編譯器分別在Mac和Linux環境下運行,并嘗試了各種優化級別。為了防止快取受到影響,我讓速度快的先執行,但有冗余代碼的回圈執行得更快。
如果你認為我的描述不可信,請將以下代碼編譯到你的電腦中并運行。希望有人能為我解答這個問題,謝謝。
C代碼:
#include <stdio.h>/span>
#include <time.h>
#include <stdlib.h>
#include <string.h>
#define TLen 300000000
#define SLen 10
int main(int argc, const char * argv[]) {
srandom((unsigned)time(NULL))。
//一個陣列來增加索引,。
//其元素的范圍是1-256。
int rand_arr[128] 。
for (int i = 0; i < 128; i)
rand_arr[i] = random()%256 1。
//一個隨機文本(很長),其元素范圍是0-127。
char *tex = malloc((sizeof *tex) * TLen) 。
for (int i = 0; i < TLen; i)
tex[i] = random()%128;
//A random string(very short))。
char *str = malloc((sizeof *str) * SLen) 。
for (int i = 0; i < SLen; i)
str[i] = random()%128。
//第一次測驗。
clock_t start = clock();
for (int i = 0; i < TLen; ) {
if (!memcmp(tex i, str, SLen)) printf("yes!
")。)
i = rand_arr[tex[i]]。
}
clock_t end = clock()。
printf("No.1: %lf s
", ((double) (end - start)) / clocks_per_sec)。)
//第二個測驗for (int i = 0; i < TLen; ) {
i = rand_arr[tex[i]]。
}
end = clock()。
printf("No.2: %lf s
", ((double) (end - start)) / clocks_per_sec)。)
return 0。
}
我已經運行了數百次,幾乎都是這個比例。下面是在Linux中測驗的一個代表性結果:
No.1。0.110000 s
沒有.2。0.140000 s
uj5u.com熱心網友回復:
關鍵的瓶頸是在跨過tex[]時,作為從舊的i到新的i的依賴鏈的一部分,快取錯過的負載使用延遲。 從小的rand_array[]中的查找將在快取中命中,并且只是在回圈攜帶的依賴鏈中增加了大約5個周期的L1d負載使用延遲,這使得除了通過i的延遲之外,其他的東西更不可能與回圈的整體速度有關。
memcmp(tex i, str, SLen)如果tex i接近快取行的末尾,可能會作為預取的行為。 如果你用-O0編譯,你正在呼叫glibc memcmp,所以在glibc memcmp中完成的SSE 16位元組或AVX2 32位元組的加載跨越了快取行的邊界并拉入下一行。 (IIRC, glibc memcmp檢查并避免page crossing, 但不包括cache-line splits, 因為這些在現代CPU上相對便宜。 單步進入它看看。 進入第二次呼叫,以避免懶惰的動態鏈接,或用-fno-plt)編譯。
可能觸及一些會被隨機stride跳過的快取行有助于HW prefetch更加積極,并將其檢測為一個順序訪問模式。 一個 32 位元組的 AVX 負載是半個高速快取行,所以它相當有可能跨越到下一個高速快取行。
因此,L2 快取看到的更可預測的快取線請求序列是 memcmp 版本更快的一個合理解釋。(Intel CPU 將主要預取器放在 L2。
如果你在編譯時進行了優化,那個 10 位元組的 memcmp 行內和內回圈中只接觸到 8 位元組。 (如果它們匹配,那么它將跳轉到另一個塊,在那里它將檢查最后兩個位元組。 但這種情況極不可能發生)。
這可能就是為什么-O0比-O3在我的系統上更快的原因,在i7-6700k Skylake上使用GCC11.1,使用DDR4-2666 DRAM,使用Arch GNU/Linux。 (你的問題沒有說明你的數字來自哪個編譯器和選項,也沒有說明哪個硬體。)
$ gcc -O3 array-stride.c
$ taskset -c 3 perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycle, instructions,uops_issued.any,uops_executed.thread,mem_load_retired.l1_hit,mem_load_retired.l1_miss -r 2 ./a.out
沒有.1。0.041710 s
沒有.2。0.063072 s
沒有.1。0.040457 s
沒有.2。0.061166 s
性能計數器統計 for './a.out' (2 運行)。
1,843.71 msec task-clock # 0. 999 CPU utilized ( - 0.14% )
0背景關系切換 # 0.000 /秒
0 個 cpu-migrations # 0.000 /秒
73,300 次頁面故障 # 39.757 K/sec ( - 0.00% )
6,607,078,798周期# 3.584 GHz ( - 0.06% )/span>
18,614,303,386條指令#每周期2.82insn ( - 0.00% )/span>
19,407,689,229 uops_issued.any # 10.526 G/sec ( - 0.02% )
21,970,261,576 uops_executed.thread # 11.916 G/sec ( - 0.02% )
5,408,090,087 mem_load_retired.l1_hit # 2.933 G/sec ( - 0.00% )
3,861,687 mem_load_retired.l1_miss # 2.095 M/sec ( - 2.11% )
1.84537 - 0.00135秒時間 elapsed ( - 0.07% )
$ grep . /sys/devices/system/cpu/cpufreq/policy*/energy_performance_preference
/sys/devices/system/cpu/cpufreq/policy0/energy_performance_preference: balance_performance
...在所有8個邏輯核心上都一樣
perf計數器資料基本上沒有意義;它是針對整個運行時間,而不是計時區域(在1.8秒中大約有0.1秒),所以這里的大部分時間是在glibc random(3),它使用"一個非線性加性反饋亂數發生器,采用大小為31長整數的默認表"。 也是在大malloc區域的頁面故障中。
這只是一個有趣的例子。
這只是在兩次構建之間的delta方面比較有趣,但是在定時區域之外的回圈仍然貢獻了許多額外的uops,所以它仍然沒有像人們希望的那樣有趣。
vs.
vs. 記住這個分析是針對總運行時間的,而不是針對定時區域的,所以2.83的IPC不是針對定時區域的。gcc -O0: No.1 更快,No.2 正如預期的那樣更慢,-O0 在涉及 i 的依賴鏈中加入了一個存盤/重新加載。
$ gcc -O0 array-stride.c
$ taskset -c 3 perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycle, instructions,uops_issued.any,uops_executed.thread,mem_load_retired.l1_hit,mem_load_retired.l1_miss -r 2 . / a.out
沒有.1。0.028402 s
沒有.2。0.076405 s
沒有.1。0.028079 s
沒有.2。0.069492 s
性能計數器統計for './a.out'(2運行)。
1,979.57 msec task-clock # 0. 999 CPU utilized ( - 0.04% )
0背景關系切換 # 0.000 /秒
0 個 cpu-migrations # 0.000 /秒
66,656 次頁面故障 # 33.672 K/sec ( - 0.00% )
7,252,728,414周期# 3.664 GHz ( - 0.02% )/span>
20,507,166,672條指令# 每周期2.83insn ( - 0.01% )/span>
22,268,130,378 uops_issued.any # 11.249 G/sec ( - 0.00% )
25,117,638,171 uops_executed.thread # 12.688 G/sec ( - 0.00% )
6,640,523,801 mem_load_retired.l1_hit # 3.355 G/sec ( - 0.01% )
3,350,518 mem_load_retired.l1_miss # 1.693 M/sec ( - 1.39% ) /-span>
1.9810591 - 0.0000934秒時間 elapsed ( - 0.00% )
失序執行器隱藏獨立作業的成本
運行那個宏融合的cmp/je uop的實際吞吐量成本并沒有增加任何瓶頸,這要感謝out-of-order exec。 甚至整個call memcmp@plt和設定args。 瓶頸是延遲,而不是前端,或者后端的負載埠,而且OOO執行視窗足夠深,可以隱藏memcmp的作業。 也請看下面的內容來了解現代CPU。 (是的,這需要大量的閱讀才能把你的頭緒搞清楚。)
通過i的回圈攜帶的依賴性去i -> tex[i] -> 間接回到i進行下一次迭代,單調地增加它。 tex[]太大,無法放入快取,而硬體預取也無法跟上這些步伐。
因此,DRAM 的帶寬可能是限制因素,甚至是 DRAM 的延遲,如果 HW 預取不能檢測并鎖定連續或每隔一個快取行的有點順序的訪問模式。
實際的分支預測非常完美,因此它可以在加載結果到達時執行(并確認預測),與等待在同一地點開始的符號擴展位元組加載的東西并行。
而且執行時間是完全一樣的。......它實際上變得快了很多。
嗯?你的時間并沒有顯示完全相同。 是的,有了那個
memcmp,它確實變得可觀地快了。
在理論上,它應該更慢。在理論上,它應該更慢。
只有在你的理論過于簡單,無法模擬現代 CPU 的情況下才會如此。 不放慢速度是很容易解釋的,因為在等待負載延遲時,有多余的失序執行吞吐量來完成該獨立作業。
- 預測現代超標量處理器上的操作的延遲需要考慮哪些因素,以及我如何手動計算這些因素?
- 每條匯編指令需要多少個CPU周期?(性能不是這樣的;CPU重疊執行多條指令;沒有一個單一的數字成本,你可以分配給每條指令,然后把它們加起來)。
對于
-O0性能來說,也可能與之相關:
- 添加一個冗余賦值,在沒有優化的情況下,可以加快代碼的編譯速度 - Sandybridge-family存盤轉發具有可變的延遲,不要過早嘗試,實際上可以讓你實作更好的延遲,也就是說,更少的瓶頸。 但我認為
-O0memcmp更快的主要原因是做一個更廣泛的負載的預取效應。
作為參考,# gcc11.1 -O0 -fPIE .L10:與-O3的代碼相比:
# RBP持有char *tex在這一點上。 .L10: # do{[/span]}。 movsx r12, r13d # sign-extend i mov rax, QWORD PTR [rbx] # str[0...7] 被重新加載,因為別名分析和可能的puts呼叫使優化器失敗。 避免使用malloc可能會有幫助。 添加r12, rbp # i tex以避免以后的索引尋址模式? 可能不是最佳選擇 cmp QWORD PTR [r12], rax # 前8位元組的memcmp je .L18 # 在.L18處的代碼會檢查接下來的2個位元組,也許會做puts,然后再跳回去。 .L5: movsx rax, BYTE PTR [r12] # sign-extending byte load to pointer width, of tex[i] . add r13d, DWORD PTR [rsp rax*4] # i = look up in rand_array[]/span> cmp r13d, 299999999 jle .L10 # }while(i < 300000000)由于
je .L18從未被占用,因此每次迭代有7個uops,所以Skylake可以在每次迭代的2個周期內自如地發布它。即使有 L1d 快取命中,通過
i(R13D) 的回圈攜帶的依賴性也是:
- 1個周期
- 1個周期。
。movsx r12, r13d- 1個周期。
。add r12, rbp- ~5周期的負載使用延遲。
movsx rax, BYTE PTR [r12]。 (不是4c,那個樂觀的特例只適用于簡單尋址模式下的暫存器本身是一個負載的直接結果。)- ~5周期負載使用延遲:
movsx rax, BYTE PTR [r12]。- ~5周期的負載使用延遲加上1周期的ALU延遲。
add r13d, DWORD PTR [rsp rax*4]/code>因此,在L1d命中的情況下,總共約有13個周期的延遲最佳,在前端留下大量的空閑 "槽",以及空閑的后端執行單元,即使呼叫實際的glibc memcmp也沒什么大不了的。
(當然,
-O0代碼更加嘈雜,所以流水線區域的一些空閑槽已經用完了,但是由于-O0代碼將i保留在記憶體中,所以dep鏈甚至更長。為什么clang用-O0(對于這個簡單的浮點和)產生低效的asm?)
記憶體瓶頸回圈中的CPU頻率,尤其是在Skylake上。初始回圈為 CPU 提供了充足的時間,使其在定時區域之前達到最大渦輪增壓,并通過先觸及分配的頁面來預發故障。 性能評估的自動方式?
如果在那些等待快取缺失負載的傳入資料的程序中,有更多的作業,更少的停頓,那么保持CPU頻率更高的效果也可能存在。 請參閱通過施加記憶體壓力來降低 CPU 頻率。
。在上述結果中,我使用 EPP =
balance_performance進行測驗。我還用
performance進行了測驗,-O0(libc memcmp)仍然比-O3(inlined memcmp)快,但所有4個回圈確實都加快了一些(有/無memcmp,以及優化與否)。
編譯器/CPU No.1 if(memcmp)No.2 plain-O0/balance_performance0.028079 s 0.069492 s 0.069492 s?
-O0/performance0.026898 s 0.053805 s 0.053805 s 0.053805 s
-O3/balance_performance0.040457 s 0.061166 0.061166 s?
-O3/performance0.035621 s 0.0474 s 0.047475 s 0.035621 s? 即使在
EPP=performance的情況下,更快的O0效應仍然存在,而且非常顯著,所以我們知道這不是just一個時鐘速度的問題。 從以前的實驗來看,performance使它保持在最大的渦輪增壓狀態,即使在其他設定將時鐘從4.2GHz降到2.7GHz的情況下。 因此,呼叫 memcmp 很可能有助于觸發更好的預取,以減少平均快取缺失延遲。不幸的是,我沒有為 PRNG 使用一個固定的種子,所以可能會有一些變化,因為隨機性對預取器來說是好是壞,就整個快取行的訪問模式而言。 而且我只是為每一個特定的運行(由 perf stat -r 2 啟動的一對中的第二個,所以希望它應該更少地受到系統波動的影響。
看起來
performance對No.2回圈(其中發生的事情較少)和-O3版本(其中發生的事情同樣較少)產生了較大的影響,這與Skylake在performance以外的EPP設定中,當一個核心幾乎只在記憶體上遇到瓶頸而不運行許多其他指令時降低時鐘速度是一致的。uj5u.com熱心網友回復:
簡短的回答,也是純粹的猜測。在第一個定時回圈中,由
tex指向的緩沖區在初始化后仍在快取中。printf做了很多復雜的事情,包括內核呼叫,任何快取行現在很可能被其他資料填滿。第二個定時回圈現在必須從 RAM 重新讀取資料。
嘗試交換回圈,和/或將
printf延遲到兩次測量之后。哦,對
memcmp的呼叫有可能出現緩沖區溢位。轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/326840.html
標籤:
上一篇:laravel錯誤"ArgumentCountErrorToofewargumentstofunctionAppHttpControllersUserController::magesend
