量實(shí)驗(yàn):黑盒推斷緩存層次與Cache Line大小的完整指南)
做這個(gè)實(shí)驗(yàn)的時(shí)候我第一反應(yīng)是有點(diǎn)懷疑的CPU 的數(shù)據(jù)手冊(cè)上都寫(xiě)著 L1 多少、L2 多少、cache line 是 64 字節(jié)為什么還要讓我寫(xiě)一個(gè) C 程序去測(cè)但真正把代碼跑起來(lái)、把曲線畫(huà)出來(lái)的那一刻我才意識(shí)到手冊(cè)給的只是標(biāo)稱(chēng)值程序見(jiàn)到的才是真實(shí)世界里的緩存行為。這個(gè)實(shí)驗(yàn)本質(zhì)上是一個(gè)黑盒測(cè)量它不依賴(lài)任何 CPU 型號(hào)信息只靠訪問(wèn)時(shí)間和緩存命中率的物理差異就能把一臺(tái)機(jī)器的緩存層次、容量、塊大小全部反推出來(lái)。這篇文章就把我完成計(jì)組實(shí)驗(yàn)5cache 大小測(cè)量與 cache line 大小測(cè)量的完整過(guò)程整理出來(lái)包括原理、代碼、讀圖方法、踩坑記錄。如果你是計(jì)算機(jī)組成原理、體系結(jié)構(gòu)相關(guān)課程的學(xué)生或者自己買(mǎi)了個(gè)新 CPU 想驗(yàn)證緩存參數(shù)這篇文章都可以直接照著做。1. 實(shí)驗(yàn)想回答的核心問(wèn)題1.1 為什么測(cè)比查手冊(cè)更有意義緩存cache是 CPU 和主存之間的一層高速緩沖?,F(xiàn)代 CPU 至少有三層緩存L1 最快但最小L3 最慢但最大。我們平時(shí)常說(shuō)的緩存也可能指 Redis 注解、HTTP 緩存、本地磁盤(pán)緩存各種術(shù)語(yǔ)容易被繞暈。但在計(jì)組實(shí)驗(yàn)里我們關(guān)心的只有 CPU 硬件緩存參數(shù)就兩個(gè)緩存總?cè)萘恳约熬彺胬锩娴淖钚》峙鋯挝弧簿褪?cache line 的大小。查手冊(cè)當(dāng)然能查到這些參數(shù)但實(shí)際行為會(huì)受很多因素影響預(yù)取器開(kāi)沒(méi)開(kāi)、虛擬內(nèi)存的頁(yè)著色、同一物理核心的超線程、多核共享 L3 時(shí)的競(jìng)爭(zhēng)都會(huì)讓性能發(fā)生變化。手冊(cè)的標(biāo)稱(chēng)值是一個(gè)理想邊界而實(shí)驗(yàn)測(cè)量出來(lái)的是一個(gè)程序可見(jiàn)的拐點(diǎn)。對(duì)于操作系統(tǒng)、編譯器、性能優(yōu)化的人來(lái)說(shuō)這個(gè)實(shí)測(cè)拐點(diǎn)才是真正有意義的。1.2 緩存容量和 cache line 分別是什么先簡(jiǎn)單對(duì)齊概念。緩存容量是指 L1、L2、L3 分別能裝多少數(shù)據(jù)。cache line 是緩存和內(nèi)存之間傳輸數(shù)據(jù)的最小單位常見(jiàn)值是 64 字節(jié)也有平臺(tái)是 128 字節(jié)部分老處理器是 32 字節(jié)。舉個(gè)例子程序想讀一個(gè) 1 字節(jié)的 charCPU 不會(huì)只把 1 字節(jié)拿回來(lái)而是把它所在的整條 cache line比如 64 字節(jié)一起載入緩存。所以程序中兩個(gè)相距 0 到 63 字節(jié)的訪問(wèn)在實(shí)際硬件上可能只觸發(fā)一次內(nèi)存讀取如果兩個(gè)訪問(wèn)相距 64 字節(jié)就一定是兩條不同的 cache line。這個(gè)特性就是本次實(shí)驗(yàn)的突破口。緩存大小影響訪問(wèn)延遲的容量拐點(diǎn)cache line 大小影響跨步訪問(wèn)的步長(zhǎng)拐點(diǎn)。只要把時(shí)間和數(shù)據(jù)規(guī)模的關(guān)系測(cè)出來(lái)兩個(gè)參數(shù)就都浮出水面了。2. 兩個(gè)指標(biāo)的測(cè)量原理拆解2.1 用容量階梯測(cè)量緩存大小假設(shè)我們有一個(gè)很大很大的數(shù)組比如 64MB。我們以 64 字節(jié)為步長(zhǎng)遍歷它也就是一次只碰一條 cache line把數(shù)組從頭掃到尾。然后不斷縮小數(shù)組規(guī)模重復(fù)同樣的遍歷記錄每次訪問(wèn)的平均耗時(shí)。原理特別直觀當(dāng)數(shù)組規(guī)模遠(yuǎn)超某一級(jí)緩存時(shí)大部分訪問(wèn)會(huì)落到下一級(jí)乃至內(nèi)存里速度明顯變慢當(dāng)數(shù)組規(guī)模剛好能被某級(jí)緩存裝下時(shí)訪問(wèn)速度就會(huì)處于一個(gè)相對(duì)平緩的臺(tái)階。如果我們從 1KB 一直測(cè)到 64MB理論上會(huì)看到三個(gè)明顯的臺(tái)階對(duì)應(yīng) L1、L2、L3 的容量。臺(tái)階交界處就是對(duì)應(yīng)緩存的容量。但順序遍歷會(huì)遇到預(yù)取器干擾?,F(xiàn)在 CPU 的硬件預(yù)取器很聰明它發(fā)現(xiàn)你在順序讀就會(huì)提前把后面的數(shù)據(jù)拉進(jìn)緩存導(dǎo)致訪問(wèn)速度看起來(lái)沒(méi)那么慢。所以更嚴(yán)謹(jǐn)?shù)淖龇ㄊ请S機(jī)訪問(wèn)。用指針追逐模式讓下一次要訪問(wèn)的地址由上一次訪問(wèn)的結(jié)果決定預(yù)取器就基本猜不中。我在后面代碼里先給出最直觀的順序遍歷版本同時(shí)補(bǔ)充指針追逐的改進(jìn)策略。2.2 用跨步拐點(diǎn)測(cè)量 cache line 大小這次我們把數(shù)組固定在一個(gè)很大的規(guī)模上比如 32MB保證它無(wú)論如何都裝不進(jìn)最大緩存。然后改變?cè)L問(wèn)步長(zhǎng)從 1 字節(jié)、2 字節(jié)、4 字節(jié)一直增加到 1024 字節(jié)統(tǒng)計(jì)每次訪問(wèn)的平均耗時(shí)。關(guān)鍵邏輯在于訪問(wèn)同一個(gè)緩存行內(nèi)的不同字節(jié)只有第一次是真的從內(nèi)存加載后續(xù)都是緩存命中。當(dāng)步長(zhǎng)小于 cache line 大小時(shí)每加載一條 cache line會(huì)有多個(gè)被訪問(wèn)到的字節(jié)落在同一條 line 內(nèi)相當(dāng)于一次內(nèi)存訪問(wèn)攤到多次程序訪問(wèn)上平均時(shí)間就低。當(dāng)步長(zhǎng)等于 cache line 大小時(shí)每條 cache line 只被訪問(wèn)一次每次訪問(wèn)都對(duì)應(yīng)一次實(shí)際的內(nèi)存加載平均時(shí)間達(dá)到峰值。步長(zhǎng)繼續(xù)增大超過(guò) cache line 大小后每條訪問(wèn)都從新的一行拿數(shù)據(jù)但實(shí)際訪問(wèn)次數(shù)變少了所以均攤下來(lái)每條的時(shí)間保持在一個(gè)高平臺(tái)上。用數(shù)字算一下假設(shè) cache line 是 64 字節(jié)數(shù)組 32MB。步長(zhǎng)為 1 字節(jié)時(shí)一次完整的遍歷會(huì)發(fā)生 32MB 次程序訪問(wèn)但內(nèi)存只真正加載了 32MB / 64 512K 次每個(gè)程序訪問(wèn)平均攤到的硬成本大概是內(nèi)存延遲的六十四分之一。步長(zhǎng)為 64 字節(jié)時(shí)程序訪問(wèn)次數(shù)是 512K每次訪問(wèn)都要跨一條新 cache line每個(gè)程序訪問(wèn)都承擔(dān)一次完整的內(nèi)存加載延遲。所以曲線上必然在步長(zhǎng) 64 附近出現(xiàn)一個(gè)突然爬升的拐點(diǎn)這個(gè)拐點(diǎn)對(duì)應(yīng)的橫坐標(biāo)就是 cache line 大小。2.3 預(yù)取器、頻率和多核對(duì)測(cè)量的干擾原理聽(tīng)起來(lái)很清爽實(shí)際上手才會(huì)發(fā)現(xiàn)臟活全在后頭。最典型的三個(gè)干擾源第一是預(yù)取器。順序訪問(wèn)時(shí)預(yù)取器會(huì)把后續(xù)幾行提前拉進(jìn)來(lái)讓內(nèi)存延遲看起來(lái)變短。對(duì)策是改用隨機(jī)化訪問(wèn)或者在同一個(gè)測(cè)試內(nèi)重復(fù)多次把預(yù)取效果壓低。第二是 CPU 頻率?,F(xiàn)代處理器有睿頻溫度一變頻率就變時(shí)間讀數(shù)就會(huì)亂飄。實(shí)驗(yàn)之前最好把測(cè)試線程綁定到固定核心并且盡量讓機(jī)器空閑不要開(kāi)一堆后臺(tái)任務(wù)。第三是多核共享。L3 是整個(gè)芯片共享的別的核也在跑程序的話L3 同時(shí)被占用測(cè)量結(jié)果會(huì)被拉偏。寫(xiě)代碼時(shí)用sched_setaffinity把進(jìn)程綁到一個(gè)核上雖然不能完全隔離 L3 競(jìng)爭(zhēng)但至少能減少一部分。3. 實(shí)驗(yàn)環(huán)境與 C 語(yǔ)言實(shí)現(xiàn)3.1 工具選擇和準(zhǔn)備我的環(huán)境是 Linux編譯器用 gcc。計(jì)時(shí)用clock_gettime(CLOCK_MONOTONIC)它返回單調(diào)時(shí)鐘不會(huì)因?yàn)槭謩?dòng)改系統(tǒng)時(shí)間而跳變精度也足夠到納秒量級(jí)。需要特別注意的是編譯器問(wèn)題。如果數(shù)組內(nèi)容簡(jiǎn)單累加編譯器可能把整個(gè)循環(huán)優(yōu)化成無(wú)意義的常量計(jì)算所以測(cè)試數(shù)組必須聲明為volatile并且用一個(gè) volatile 變量承接讀取結(jié)果確保每次內(nèi)存訪問(wèn)都真實(shí)發(fā)生。還要做兩件事關(guān)掉會(huì)拉偏測(cè)試的 CPU 遷移用sched_setaffinity綁定到 0 號(hào)核心如果機(jī)器支持也可以考慮關(guān)掉超線程后再測(cè)減少同核爭(zhēng)搶。3.2 實(shí)驗(yàn)一代碼測(cè)量緩存大小核心思路是讓數(shù)組容量從 1KB 增長(zhǎng)到 64MB固定步長(zhǎng) 64 字節(jié)記錄每次訪問(wèn)的平均納秒數(shù)。#include stdio.h #include stdlib.h #include string.h #include time.h #include sched.h #define STRIDE 64 #define MAX_SIZE (64 * 1024 * 1024) static volatile unsigned char pool[MAX_SIZE]; static double now_ns(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, ts); return (double)ts.tv_sec * 1e9 (double)ts.tv_nsec; } static double run(int bytes, int loops) { volatile unsigned char sink 0; double t0, t1; int i, j; // 預(yù)熱把訪問(wèn)過(guò)的頁(yè)面提前碰一遍避免把缺頁(yè)時(shí)間算進(jìn)去 for (i 0; i bytes; i STRIDE) sink pool[i]; t0 now_ns(); for (j 0; j loops; j) { for (i 0; i bytes; i STRIDE) sink pool[i]; } t1 now_ns(); return (t1 - t0) / (double)((long)loops * (bytes / STRIDE)); } int main(void) { int kb[] {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 6144, 8192, 12288, 16384, 32768, 65536}; int i; memset((void *)pool, 0, sizeof(pool)); // 綁定到 0 號(hào)核心 cpu_set_t set; CPU_ZERO(set); CPU_SET(0, set); sched_setaffinity(0, sizeof(set), set); for (i 0; i sizeof(kb) / sizeof(kb[0]); i) { int bytes kb[i] * 1024; int lines bytes / STRIDE; int loops 16 * 1024 * 1024 / lines; if (loops 1) loops 1; double ns run(bytes, loops); printf(%8d KB %10.3f ns/access\n, kb[i], ns); } return 0; }這段代碼的關(guān)鍵在loops的調(diào)整。數(shù)組變小的時(shí)候每輪訪問(wèn)到的 cache line 數(shù)量少所以要多跑幾輪保證每個(gè)規(guī)模點(diǎn)的總訪問(wèn)量差不多避免小數(shù)組因?yàn)榕艿锰?、?jì)時(shí)器精度不夠?qū)е抡`差。volatile unsigned char sink是為了告訴編譯器每次讀取的結(jié)果都可能改變從而保住每一次內(nèi)存訪問(wèn)。編譯時(shí)用gcc -O2 -o cache_size cache_size.c -lrt-lrt在老版本 glibc 上需要新系統(tǒng)一般可以省略。如果你想更嚴(yán)格可以在這段順序遍歷的基礎(chǔ)上改成指針追逐版先生成一個(gè)大小為bytes/4的隨機(jī)索引鏈然后從鏈頭開(kāi)始一步步pos next[pos]。這樣每條指令的地址依賴(lài)上一次結(jié)果預(yù)取器基本沒(méi)法發(fā)揮作用L1、L2、L3 之間的臺(tái)階會(huì)更清晰。3.3 實(shí)驗(yàn)二代碼測(cè)量 cache line 大小這次數(shù)組固定為 32MB保證大于大多數(shù) CPU 的 L3 容量。步長(zhǎng)從 1 字節(jié)逐步增大到 1024 字節(jié)計(jì)算每次訪問(wèn)的平均耗時(shí)。#include stdio.h #include stdlib.h #include string.h #include time.h #include sched.h #define MAX_SIZE (32 * 1024 * 1024) static volatile unsigned char pool[MAX_SIZE]; static double now_ns(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, ts); return (double)ts.tv_sec * 1e9 (double)ts.tv_nsec; } int main(void) { int stride[] {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024}; int i; memset((void *)pool, 0, sizeof(pool)); cpu_set_t set; CPU_ZERO(set); CPU_SET(0, set); sched_setaffinity(0, sizeof(set), set); for (i 0; i sizeof(stride) / sizeof(stride[0]); i) { int s stride[i]; long access_count MAX_SIZE / s; int loops 64 * 1024 * 1024 / access_count; if (loops 4) loops 4; volatile unsigned char sink 0; double t0, t1; int j, k; // 預(yù)熱 for (k 0; k MAX_SIZE; k s) sink pool[k]; t0 now_ns(); for (j 0; j loops; j) { for (k 0; k MAX_SIZE; k s) sink pool[k]; } t1 now_ns(); double ns (t1 - t0) / (double)((long)loops * access_count); printf(stride %4d B, %10.3f ns/access\n, s, ns); } return 0; }這段代碼的運(yùn)行邏輯是步長(zhǎng)為 1 時(shí)access_count很大loops就會(huì)被壓小步長(zhǎng)為 1024 時(shí)每輪訪問(wèn)次數(shù)很少loops會(huì)變大。這是為了讓總訪問(wèn)次數(shù)在同一個(gè)量級(jí)從而讓每毫秒的計(jì)時(shí)誤差不至于在數(shù)據(jù)點(diǎn)上造成完全不可信的波動(dòng)。步長(zhǎng)范圍建議根據(jù)實(shí)際 cache line 大小調(diào)整。常見(jiàn)的 x86 平臺(tái) cache line 是 64 字節(jié)所以我在 32 和 64 之間多插了一個(gè)點(diǎn)。有些 ARM 平臺(tái)是 128 字節(jié)那你在 64、128、256 之間可以再加幾個(gè)點(diǎn)比如 96、160把拐點(diǎn)找得更準(zhǔn)。注意步長(zhǎng)最好保持 2 的冪這樣與緩存索引和組映射的關(guān)系更清晰。3.4 編譯、運(yùn)行和結(jié)果采集兩個(gè)程序編譯后直接運(yùn)行即可普通用戶權(quán)限就夠不需要 root。輸出是文本方便你重定向到文件后用 Python、Excel 或 gnuplot 畫(huà)圖。gcc -O2 -o cache_size cache_size.c gcc -O2 -o cache_line cache_line.c ./cache_size size_result.txt ./cache_line line_result.txt畫(huà)圖時(shí)橫軸建議用對(duì)數(shù)坐標(biāo)。緩存大小測(cè)試橫軸是數(shù)組容量從 KB 到 MB差異可能上千倍線性坐標(biāo)根本看不出早段的細(xì)節(jié)cache line 測(cè)試步長(zhǎng)從 1 到 1024也要用對(duì)數(shù)坐標(biāo)才直觀。4. 實(shí)測(cè)數(shù)據(jù)與讀圖方法4.1 預(yù)期曲線形態(tài)現(xiàn)代 x86-64 桌面處理器的典型緩存參數(shù)大致是層級(jí)典型容量典型訪問(wèn)延遲L132KB ~ 64KB1 ~ 1.3nsL2256KB ~ 2MB4 ~ 10nsL38MB ~ 32MB20 ~ 50ns主存無(wú)上限60 ~ 120ns因此緩存大小測(cè)試的輸出曲線應(yīng)該是在 32KB 附近出現(xiàn)第一次跳升L2 邊界出現(xiàn)第二次跳升L3 邊界出現(xiàn)第三次跳升。三次跳升把曲線分成四段平緩區(qū)這個(gè)階梯非常明顯。cache line 測(cè)試的輸出曲線應(yīng)該是步長(zhǎng)在 1 到 32 字節(jié)之間時(shí)每次訪問(wèn)的平均時(shí)間相對(duì)平穩(wěn)在步長(zhǎng) 64 附近突然變高之后保持在偏高平臺(tái)。拐點(diǎn)橫坐標(biāo)直接等于 cache line 大小。4.2 縱軸和橫軸讀法緩存大小測(cè)試的縱軸是每次訪問(wèn) cache line 的平均耗時(shí)單位 ns/access。這里說(shuō)的一次訪問(wèn)是指一次pool[i]讀取由于步長(zhǎng)固定為 64 字節(jié)它近似等于訪問(wèn)一條 cache line的成本。緩存行測(cè)試的縱軸含義不同??v軸是每訪問(wèn)一個(gè)元素的時(shí)間元素大小是 1 字節(jié)。當(dāng)步長(zhǎng)小的時(shí)候一次內(nèi)存加載能覆蓋多個(gè)元素均攤成本低當(dāng)步長(zhǎng)等于 cache line 大小時(shí)一次訪問(wèn)就要承擔(dān)一次完整的內(nèi)存加載成本高。所以這個(gè)實(shí)驗(yàn)的縱軸不是純?cè)L問(wèn)延遲而是均攤到每個(gè)邏輯元素上的平均延遲。這個(gè)區(qū)別如果不說(shuō)明白畫(huà)圖的時(shí)候很容易誤解成 cache line 越大越慢。4.3 我的一臺(tái)實(shí)際測(cè)試機(jī)結(jié)果在一臺(tái) L1 32KB、L2 256KB、L3 8MB 的機(jī)器上緩存大小測(cè)試得到的數(shù)據(jù)大概是測(cè)試規(guī)模每訪問(wèn)平均耗時(shí)8KB約 0.4 ns32KB約 0.9 ns64KB約 1.8 ns256KB約 3.5 ns1MB約 7.5 ns8MB約 14 ns16MB約 22 ns64MB約 24 ns可以看到在 32KB 附近曲線第一次抬頭對(duì)應(yīng) L1 容量在 256KB 附近第二次抬頭對(duì)應(yīng) L28MB 之后增幅變緩對(duì)應(yīng) L3 容量。為什么 L3 平臺(tái)不如前兩級(jí)那么陡因?yàn)楝F(xiàn)代 CPU 預(yù)取器和跨步訪問(wèn)策略對(duì) L3 訪問(wèn)的掩蓋作用比較強(qiáng)但拐點(diǎn)依然可辨。cache line 測(cè)試部分的數(shù)據(jù)也符合預(yù)期步長(zhǎng) 1 到 32 字節(jié)的平均訪問(wèn)時(shí)間大約在 1.2 到 2.0ns 之間步長(zhǎng) 64 突然跳到 8ns 以上。這說(shuō)明 cache line 邊界就是 64 字節(jié)和手冊(cè)上的參數(shù)完全吻合。5. 常見(jiàn)問(wèn)題與排查實(shí)錄5.1 編譯器把測(cè)試代碼優(yōu)化成什么都沒(méi)做這是最容易踩的坑。如果你沒(méi)把測(cè)試數(shù)組聲明成volatile編譯器會(huì)認(rèn)為整個(gè)循環(huán)的累加結(jié)果從來(lái)沒(méi)被使用直接把它刪除或合并。你測(cè)到的不是內(nèi)存訪問(wèn)時(shí)間而是循環(huán)空轉(zhuǎn)的耗時(shí)甚至可能快到一個(gè)不合常理的數(shù)值比如每訪問(wèn) 0.001ns。解決方法是三件套測(cè)試數(shù)組全局聲明為volatile承接結(jié)果的變量也聲明為volatile編譯時(shí)用-O2而不是-O0。只要三點(diǎn)做到編譯器就沒(méi)法偷懶。5.2 曲線臺(tái)階不明顯全是鋸齒原因主要是預(yù)取器和系統(tǒng)噪聲。我遇到過(guò)的情況是機(jī)器后臺(tái)有索引服務(wù)在跑L3 時(shí)快時(shí)慢曲線在 16MB 到 64MB 之間上下抖動(dòng) 30%。處理辦法先把后臺(tái)程序盡量停掉然后把測(cè)試進(jìn)程綁定到一個(gè)固定核心。如果還不行可以嘗試關(guān)閉 CPU 硬件預(yù)取但普通環(huán)境下沒(méi)有 root 權(quán)限通常改不了 MSR所以我一般是用隨機(jī)訪問(wèn)模式做替代不再依賴(lài)順序遍歷。隨機(jī)訪問(wèn)會(huì)讓每個(gè)數(shù)據(jù)點(diǎn)都被真實(shí)未命中主導(dǎo)鋸齒會(huì)小很多。5.3 計(jì)時(shí)器分辨率不夠有些虛擬機(jī)或老內(nèi)核里clock_gettime的分辨率可能只有幾微秒而我們測(cè)量一次訪問(wèn)只有零點(diǎn)幾納秒到幾十納秒直接測(cè)單次訪問(wèn)肯定不行。代碼里已經(jīng)做了多次循環(huán)取平均但如果你的機(jī)器特別老可以把loops那一行的基準(zhǔn)值從16 * 1024 * 1024提高到64 * 1024 * 1024讓總耗時(shí)放大到毫秒級(jí)。如果是在虛擬化環(huán)境里測(cè)我的建議是放棄這個(gè)實(shí)驗(yàn)去實(shí)體 Linux 機(jī)器上跑。虛擬機(jī)的時(shí)間切片和中斷注入會(huì)讓曲線變成瘋子測(cè)出來(lái)的數(shù)據(jù)只能作為相對(duì)趨勢(shì)參考不能當(dāng)真實(shí)硬件參數(shù)。5.4 打開(kāi)perf驗(yàn)證硬件計(jì)數(shù)器時(shí)間測(cè)量本質(zhì)上是間接推斷如果想確認(rèn)緩存大小臺(tái)階確實(shí)對(duì)應(yīng)緩存失效可以在 Linux 下用perf stat看硬件計(jì)數(shù)器。比如緩存大小測(cè)試跑到 32KB 和 64KB 兩個(gè)規(guī)模時(shí)分別看 L1 緩存失效次數(shù)perf stat -e cache-references,cache-misses,l1d-loads,l1d-load-misses ./cache_sizeperf stat輸出是整個(gè)程序的總計(jì)不方便按規(guī)模拆分。更精細(xì)的做法是在 C 代碼里調(diào)用perf_event_open在每個(gè)測(cè)量點(diǎn)前后讀一次硬件計(jì)數(shù)器。不過(guò)對(duì)大多數(shù)實(shí)驗(yàn)課來(lái)說(shuō)用時(shí)間曲線已經(jīng)足夠了。硬件計(jì)數(shù)器主要用于驗(yàn)證為什么這里會(huì)拐彎屬于錦上添花。5.5 多核緩存共享帶來(lái)的誤判如果你在一顆 8 核開(kāi)滿任務(wù)的情況下測(cè) L3測(cè)出來(lái)的 L3 容量可能不足因?yàn)橛幸徊糠直黄渌藫屨剂?。更隱蔽的是同一顆物理核心的超線程也會(huì)共享 L1 和 L2如果另一個(gè)邏輯核在跑任務(wù)你的 L1、L2 就會(huì)被擠壓。我的經(jīng)驗(yàn)是先用taskset -c 0或者代碼里的sched_setaffinity綁定核心再用top或者h(yuǎn)top確認(rèn) 0 號(hào)核心基本空閑然后才開(kāi)始測(cè)。如果機(jī)器是大小核架構(gòu)比如 Intel 12 代以后的 P 核和 E 核最好綁定到一個(gè)固定的 P 核上大小核混跑會(huì)讓數(shù)據(jù)出現(xiàn)兩套完全不同的曲線。6. 最后說(shuō)點(diǎn)個(gè)人體會(huì)這個(gè)實(shí)驗(yàn)做完之后我對(duì)緩存是什么的理解完全不一樣了。以前背的L1 32KB、L2 256KB、cache line 64 字節(jié)只是紙面上的數(shù)字現(xiàn)在我看一條曲線就能直接說(shuō)出這臺(tái)機(jī)器緩存分幾層、每層大概多大。這種能力在調(diào)優(yōu)內(nèi)存訪問(wèn)密集型程序的時(shí)候特別有用比如矩陣分塊、池化分配、鏈表轉(zhuǎn)數(shù)組這些優(yōu)化本質(zhì)上都是在順應(yīng)緩存的行為。實(shí)際跑實(shí)驗(yàn)時(shí)我個(gè)人建議不要只跑一遍。多跑兩三遍取每次讀數(shù)的中位數(shù)而不是平均數(shù)因?yàn)槠骄鶖?shù)容易被偶發(fā)的中斷拉高。如果你用 Python 處理結(jié)果可以直接畫(huà)一張雙對(duì)數(shù)圖把緩存大小測(cè)試和 cache line 測(cè)試兩條曲線放在一起很多規(guī)律一眼就看出來(lái)了。這個(gè)實(shí)驗(yàn)后續(xù)還能繼續(xù)擴(kuò)展比如測(cè)量緩存相聯(lián)度、測(cè)量不同寫(xiě)分配策略的效果甚至改成用rdtsc指令做高精度計(jì)時(shí)。每次換一種測(cè)法都能從 CPU 這個(gè)黑盒里多撬出一層真相。