7.2 模擬處理器快取行為
就算快取的運作原理相當容易理解,但實際程式究竟如何與快取互動並不容易觀察。其實程式開發者並不在乎資料所在的記憶體位置。記憶體位址產生是由連結器 (linker) 或運行當下由動態連結器與作業系統決定。產生的組合語言能夠處理所有可能的記憶體定址。因此程式碼中甚至看不到任何關於記憶體位置的線索。也因為如此要理解程式如何使用記憶體真的非常困難。
我們在 7.1 節所提到的 cpu 層面的分析工具像 oprofile 有助於我們了解快取的使用情況。所有數據都直接由硬體收集,在不追求高精度的狀況下十分好用。一旦牽涉高精度需求的資料時,oprofile 就不適用;因為執行緒可能會被中斷非常多次。因此若要查看程式在不同處理器上的記憶體使用行為,實際上必須湊齊這些不同的機台並直接在上面做實驗,但往往窒礙難行。像圖 3.8 的數據。若是要使用 oprofile 收集這些資料,必須要有 24 台不同的機器,但其中許多機器根本不存在。
==19645== I refs: 152,653,497
==19645== I1 misses: 25,833
==19645== L2i misses: 2,475
==19645== I1 miss rate: 0.01%
==19645== L2i miss rate: 0.00%
==19645==
==19645== D refs: 56,857,129 (35,838,721 rd + 21,018,408 wr)
==19645== D1 misses: 14,187 ( 12,451 rd + 1,736 wr)
==19645== L2d misses: 7,701 ( 6,325 rd + 1,376 wr)
==19645== D1 miss rate: 0.0% ( 0.0% + 0.0% )
==19645== L2d miss rate: 0.0% ( 0.0% + 0.0% )
==19645==
==19645== L2 refs: 40,020 ( 38,284 rd + 1,736 wr)
==19645== L2 misses: 10,176 ( 8,800 rd + 1,376 wr)
==19645== L2 miss rate: 0.0% ( 0.0% + 0.0% )
圖 7.5:cachegrind 統計結果
圖中是使用快取模擬器所得到的結果。cachegrind
使用 valgrind
框架是為了檢查程式中的記憶體運用相關問題所開發。valgrind
模擬程式執行過程並在執行期間可使用各種擴充功能,例如 cachegrind
可竄入執行框架。cachegrind
利用這個功能來攔截紀錄所有記憶體地址的使用狀況,然後模擬所有 L1i
、L1d
和 L2
快取在任何指定資料容量的行為。若要使用這個功能,必須在使用 valgrind
時給定參數如下:
valgrind --tool=cachegrind command arg
上圖的範例同時模擬三個快取以及對應的資料容量與處理器。當程式在運行時,部分結果會寫入到標準錯誤(stderr),其中包含快取總使用量的統計結果,如圖 7.5 所示。圖中顯示指令和記憶體位置參考的總數以及 L1i/L1d
和 L2
快取產生的未命中次數、未命中率等等。該工具甚至可將 L2
存取細分為指令存取和資料存取,所有資料快取都區分為讀取和寫入二種不同行為。
有了基礎的認知後我們可透過輸入不同的參數來進行更複雜的操作。 使用 --I1
、 --D1
和 --L2
參數, cachegrind
可忽略實體處理器的快取結構,模擬參數所指定的結構。 如下面的命令:
valgrind --tool=cachegrind \
--L2=8388608,8,64 command arg
模擬 8MB 的 L2 快取容量並支援 8 路組 (8-way set) , 每個快取行的容量為 64 位元。 注意 --L2
需要放在 command arg
的前面。
此外 cachegrind
還提供更多功能。在行程結束之前會產生一個 cachegrind.out.XXXXX
檔,其中 XXXXX
是行程的 PID
。這個檔案包含程式中每個函式和原始程式碼中快取使用的摘要和詳細內容。可使用 cg annotate
來查看這些資料。
程式所產生的結果包含快取使用摘要,以及每個函式對快取使用的詳細摘要。要獲得資料需要 cg annotate
將記憶體位置與函式做配對,這表示最好要有除錯相關的資訊。雖然檔案可提供一些幫助,但由於內部符號 (internal symbol) 沒有列在動態符號表中,因此結果不夠完整。圖 7.6 顯示了與圖 7.5 相同的程式運行的部分結果。
--------------------------------------------------------------------------------
Ir I1mr I2mr Dr D1mr D2mr Dw D1mw D2mw file:function
--------------------------------------------------------------------------------
53,684,905 9 8 9,589,531 13 3 5,820,373 14 0 ???:_IO_file_xsputn@@GLIBC_2.2.5
36,925,729 6,267 114 11,205,241 74 18 7,123,370 22 0 ???:vfprintf
11,845,373 22 2 3,126,914 46 22 1,563,457 0 0 ???:__find_specmb
6,004,482 40 10 697,872 1,744 484 0 0 0 ???:strlen
5,008,448 3 2 1,450,093 370 118 0 0 0 ???:strcmp
3,316,589 24 4 757,523 0 0 540,952 0 0 ???:_IO_padn
2,825,541 3 3 290,222 5 1 216,403 0 0 ???:_itoa_word
2,628,466 9 6 730,059 0 0 358,215 0 0 ???:_IO_file_overflow@@GLIBC_2.2.5
2,504,211 4 4 762,151 2 0 598,833 3 0 ???:_IO_do_write@@GLIBC_2.2.5
2,296,142 32 7 616,490 88 0 321,848 0 0 dwarf_child.c:__libdw_find_attr
2,184,153 2,876 20 503,805 67 0 435,562 0 0 ???:__dcigettext
2,014,243 3 3 435,512 1 1 272,195 4 0 ???:_IO_file_write@@GLIBC_2.2.5
1,988,697 2,804 4 656,112 380 0 47,847 1 1 ???:getenv
1,973,463 27 6 597,768 15 0 420,805 0 0 dwarf_getattrs.c:dwarf_getattrs
圖 7.6:cg annotate 結果
Ir
、Dr
和 Dw
這幾個欄位顯示的是總快取使用量,而非快取未命中數,這些未命中次數會在後面二欄顯示。這些數據可用於辨識出發生最多快取未命中的程式碼。首先可先關注 L2
的快取未命中,再延伸處理 L1i/L1d
快取未命中。
cg annotate
可提供更詳細的資料。如果有特別指定某一個檔案,甚至可逐行顯示出對應的快取命中數和未命中數。這些資訊可讓程式開發者深入到確切行數,雖然使用介面粗糙。本文書寫之際,cachegrind 資料檔案和原始檔必須在同一個目錄中。
再次強調,cachegrind
是模擬器,不會使用來自處理器的測量數據。因此與處理器中實際的快取實作方式很可能會非常不同。cachegrind
使用最近最少使用 (LRU) 策略,但對具高關聯性的高速快取而言,這會造成額外的成本。除此之外,模擬器並沒有考慮上下文交換 (context switch) 和系統呼叫 (system call)的成本。這二者都可能破壞大部分 L2 快取且必須更新 L1i 和 L1d 的快取內容。會導致模擬結果的總快取未命中數低於實際的狀況。儘管如此,cachegrind
仍是一個很好的工具,可了解程式的記憶體使用情況以及其相關的問題。