6.2.3. 最佳化二階與更高階快取存取
關於一階快取的最佳化所說的一切也適用於二階與更高階快取存取。有兩個最後一層快取的額外面向:
- 快取錯失一直都非常昂貴。L1 錯失(希望)頻繁地命中 L2 與更高階快取,於是限制了其損失,但最後一層快取顯然沒有後盾了。
- L2 快取與更高階快取經常由多顆核心與/或超執行緒所共享。每個執行單元可用的有效快取大小因而經常小於總快取大小。
為了避免快取錯失的高成本,工作集大小應該配合快取大小。若是資料只需要一次,這顯然不是必要的,因為快取無論如何都沒有效果。我們要討論的是被需要不只一次的資料集的工作負載。在這種情況下,使用一個太大而不能塞得進快取的工作集將會產生大量的快取錯失,即使預取成功地執行了,也會拖慢程式。
即使資料集太大,一支程式也必須完成它的職責。以最小化快取錯失的方式完成工作是程式設計師的職責。對於末層快取,是可能––如同 L1 快取––以較小的部分來執行工作的。這與表 6.2 最佳化的矩陣乘法非常雷同。不過,有一點不同在於,對於最後一層快取,要處理的資料區塊可能比較大。如果也需要 L1 最佳化,程式會變得更加複雜。想像一個矩陣乘法,其資料集––兩個輸入矩陣與輸出矩陣––無法同時塞進最後一層快取。在這種情況下,或許適合同時最佳化 L1 與最後一層快取存取。
眾多處理器世代中的 L1 快取行大小經常是固定的;即使不同,差異也很小。假設為較大的大小是沒什麼大問題的。在有著較小快取大小的處理器中,會用到兩個或更多快取行、而非一個。在任何情況下,寫死快取行大小、並為此最佳化程式都是合理的。
對於較高層級的快取,若程式是假定為一般化的話,就不是這樣了。那些快取的大小可能有很大的差異。八倍或更多倍並不罕見。將較大的快取大小假定為預設大小是不可能的,因為這可能表示,除了那些有著最大快取的機器之外,程式在所有機器上都會表現得很差。相反的選擇也很糟:假定為最小的快取,代表浪費掉 87% 或者更多的快取。這很糟;如同我們能從圖 3.14 看到的,使用大快取對程式的速度有著巨大的影響。
這表示程式必須動態地將自身調整為快取行大小。這是一種程式特有的最佳化。我們這裡能說的是,程式設計師應該正確地計算程式的需求。不僅資料集本身需要,更高層級的快取也會被用於其它目的;舉例來說,所有執行的指令都是從快取載入的。若是使用了函式庫函式,這種快取的使用可能會加總為一個可觀的量。那些函式庫函式也可能需要它們自己的資料,進一步減少了可用的記憶體。
一旦我們有一個記憶體需求的公式,我們就能夠將它與快取大小作比較。如同先前所述,快取可能會被許多其它核心所共享。當前34,在沒有寫死知識的情況下,取得正確資訊的唯一方法是透過 /sys
檔案系統。在表 5.2,我們已經看過系統核心發布的有關於硬體的資訊。程式必須在目錄:
/sys/devices/system/cpu/cpu*/cache
找到最後一層快取。這能夠由在這個目錄裡的層級檔案中的最高數值來辨別出來。當目錄被識別出來時,程式應該讀取在這個目錄中的 size
檔案的內容,並將數值除以 shared_cpu_map
檔案中的位元遮罩中設置的數字。
以這種方式計算的值是個安全的下限。有時一支程式會知道多一些有關其它執行緒或行程的行為。若是那些執行緒被排程在共享這個快取的核心或超執行緒上、並且已知快取的使用不會耗盡它在總快取大小中所佔的那份,那麼計算出的限制可能會太小,而不是最佳的。是否要比公平共享應該使用的還多,真的要視情況而定。程式設計師必須做出抉擇,或者必須讓使用者做個決定。
34. 當然很快就會有更好的方法! ↩