軟體預讀機制由來已久,首先實現預讀指令的處理器是
motorola
的88110
處理器,這顆處理器首先實現了
touch load
指令,這條指令是
powerpc
處理器dcbt
指令[4]
的前身。後來絕大多數處理器都採用這類指令進行軟體預讀,
intel
在i486
處理器中使用
dummy read
指令,這條指令也是後來
x86處理器中
prefetchh
[5]指令的雛形。
使用軟體預讀指令可以在處理器真正需要資料之前,向儲存器預先發出讀請求,這個預讀請求不需要等待資料真正到達儲存器之後,就可以執行完畢,以實現儲存器訪問與處理器運算同步進行,從而提高了任務的整體執行效率。
除了專有指令外,普通的讀指令也可以用作預讀,如
non-blocking
的load
指令。這個讀指令與
prefetch
指令最大的區別是,這些指令不僅將資料引入
cache
層次結構,而且會將結果寫入某個暫存器,這類指令也被稱為
binding prefetch
。與此對應,在微架構中專門設定的
prefetch
指令被稱為
non-binding prefetch
指令。
prefetch
指令需要採用
non-blocking
,non-exception-generating
方式實現。
non-blocking
較易理解,因為在乙個使用
blocking cache
的微架構中,沒有使用
prefetch
指令的任何必要。在微架構中,乙個簡單實現
prefetch
指令的做法是借用
non-blocking load
指令,並將結果傳遞給
nobody
暫存器,較為複雜的實現是預讀資料的同時,引入一些
hint
,如微架構將如何使用預讀的資料,是寫還是讀,這些資訊有助於多核處理器的一致性處理。
non-exception-generating
指在prefetch
時不得引發
exception
,包括page fault
和其他各類的
memory exception
。在一些微架構中如果
prefech
引發了exception
,獲得的資料將被丟棄。此外
exception
還會帶來較大的
overhead
,對memory consistency
的實現製造障礙。
軟體預讀指令可以由編譯器自動加入,但是在很多場景,更加有效的方式是由程式設計師主動加入預讀指令。這些預讀指令在進行大規模向量運算時,可以發揮巨大的作用。在這一場景中,通常含有大規模的有規律的
loop iteration
。這類程式通常需要訪問處理較大規模的資料,從而在一定程度上破壞了程式的
temporal locality
和spatial locality
,這使得資料預讀成為提高系統效率的有效手段。我們考慮圖5
‑2中的例項。
這個例子在進行向量運算時被經常使用,這段源**的作用是將
int型別的陣列
a和陣列
b的每一項進行相乘,然後賦值給
ip,其中陣列a和
b的基位址
cache block
對界。我們假設
n為乙個較大的常數且能夠被
4整除,此外微架構的
cache block為32
位元組,並在此基礎上考慮圖5
‑2中的幾個例項。
在例項a中沒有使用預讀機制進行優化。這段程式在執行時,
a[i]
和b[i]
中的資料不會在處理器的
cache
中命中,而且在順序訪問向量a和
b的資料單元時,每次跨越
cache block
都會因為
compulsory misses
向儲存器子系統傳送讀請求,從而
stall
微架構的指令流水,降低了程式的執行效率。
例項b在對變數
ip賦值之前,首先對陣列a和
b進行預讀,當對變數
ip賦值時,陣列a和
b中的資料可能已經在
cache
中,從而在一定程度上提高了**的執行效率。這段**並不完美。因為在絕大多數微架構中,預讀以
cache block
為單位進行,對
a[0], a[1], a[2], a[3]
進行預讀時都是對同乙個
cache block
進行預讀。因此這段**對同乙個
cache block
進行了多次預讀,從而影響了執行效率。
例項c使用
loop unrolling
技術,將迴圈體內的賦值操作進一步展開為
4個子步驟,從而避免了例項
b中存在的多次預讀。在現代處理器中,
branch prediction
較為完善,此處出現的
loop unrolling
並不會降低迴圈轉移的開銷,其主要目的是提高
cache block
的利用率,以減少預取次數。
例項d是在
c基礎上的繼續優化,借用流水線設計的思想,將一次計算,分解為
prolog
,main loop
和epilog
三個階段。其中
prolog
是建立流水時的準備工作,
main loop
是預讀與計算的並行階段,而
epilog
是最後的結尾工作。
以上這些方法較為通用,有些編譯器會自動將例項
a轉化為例項
d。但是這些優化方式仍然忽略了乙個細節,由於儲存器的訪問延時,預讀的資料可能不會在計算需要時及時達到,指令流水線依然會
stall
。為此預讀指令需要進一步考慮儲存器延時與計算所需時間之間的關係,保證預讀的資料在計算需要時準時到達。
為此我們需要對
prefetchdistance
引數做進一步分析,該引數簡稱為
δ,其計算公式為
δ = ceiling(l/s)
[100]
。其中l
為平均儲存器訪問延時,而
s為乙個
loop iteration
中計算部分使用的最短執行時間。
假設在例項
d中,平均儲存器訪問延時為
100個時鐘週期,而乙個
loop iteration
中的計算使用的最短執行時間為
45個時鐘週期時,
δ引數的值為
3。這一結果表明每次預讀指令需要在3倍於
loop iteration
中的計算時間之前執行,才能保證軟體流水可以順利進行,不會因為預讀的資料尚未到達而被迫等待。使用
prefetch distance
引數可以進一步優化例項d,如
圖5‑3所示。
這些優化並不是軟體預讀的終點,還有很多利用某些
cache
深層次特性做進一步優化的可能。這些優化都是具有一定的針對性,需要對處理器體系結構有著較為深刻的理解。在很多情況下軟體預讀機制有較為明顯的缺點,首先是
code expansion
的問題,軟體預讀優化增加了**長度,在一定程度上容易造成
l1 cache
的pollution
,其次是預讀指令本身的所帶來的
overhead
物理讀,邏輯讀,預讀
在使用set statistics io on語句統計i o時候,我們會看到類似下面的結果 掃瞄計數 1,邏輯讀取 2 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。那麼它們代表什麼呢?預讀 用於估計資訊,去硬碟讀取資料到快取。物理讀 ...
SQL Server邏輯讀 預讀和物理讀
預讀 用估計資訊,去硬碟讀取資料到快取。預讀100次,也就是估計將要從硬碟中讀取了100頁資料到快取。物理讀 查詢計畫生成好以後,如果快取缺少所需要的資料,讓快取再次去讀硬碟。物理讀10頁,從硬碟中讀取10頁資料到快取。邏輯讀 從快取中取出所有資料。邏輯讀100次,也就是從快取裡取到100頁資料。s...
SQLSERVER預讀邏輯讀物理讀
預讀 用估計資訊,去硬碟讀取資料到快取。預讀100次,也就是估計將要從硬碟中讀取了100頁資料到快取。物理讀 查詢計畫生成好以後,如果快取缺少所需要的資料,讓快取再次去讀硬碟。物理讀10頁,從硬碟中讀取10頁資料到快取。邏輯讀 從快取中取出所有資料。邏輯讀100次,也就是從快取裡取到100頁資料。l...