产生线程安全的原因(2)(操作系统)
3.3.2 Cache的性能測試
用于測試程序的數據可以模擬一個任意大小的工作集:包括讀、寫訪問,隨機、連續訪問。在圖3.4中我們可以看到,程序為工作集創建了一個與其大小和元素類型相同的數組:
struct l {struct l *n;long int pad[NPAD]; };n字段將所有節點隨機得或者順序的加入到環形鏈表中,用指針從當前節點進入到下一個節點。pad字段用來存儲數據,其可以是任意大小。在一些測試程序中,pad字段是可以修改的, 在其他程序中,pad字段只可以進行讀操作。
在性能測試中,我們談到工作集大小的問題,工作集使用結構體l定義的元素表示的。2N?字節的工作集包含
個元素. 顯然sizeof(struct l) 的值取決于NPAD的大小。在32位系統上,NPAD=7意味著數組的每個元素的大小為32字節,在64位系統上,NPAD=7意味著數組的每個元素的大小為64字節。
單線程順序訪問
最簡單的情況就是遍歷鏈表中順序存儲的節點。無論是從前向后處理,還是從后向前,對于處理器來說沒有什么區別。下面的測試中,我們需要得到處理鏈表中一個元素所需要的時間,以CPU時鐘周期最為計時單元。圖3.10顯示了測試結構。除非有特殊說明, 所有的測試都是在Pentium?4 64-bit 平臺上進行的,因此結構體l中NPAD=0,大小為8字節。
一開始的兩個測試數據收到了噪音的污染。由于它們的工作負荷太小,無法過濾掉系統內其它進程對它們的影響。我們可以認為它們都是4個周期以內的。這樣一來,整個圖可以劃分為比較明顯的三個部分:
- 工作集小于214字節的。
- 工作集從215字節到220字節的。
- 工作集大于221字節的。
這樣的結果很容易解釋——是因為處理器有16KB的L1d和1MB的L2。而在這三個部分之間,并沒有非常銳利的邊緣,這是因為系統的其它部分也在使用緩存,我們的測試程序并不能獨占緩存的使用。尤其是L2,它是統一式的緩存,處理器的指令也會使用它(注: Intel使用的是包容式緩存)。
測試的實際耗時可能會出乎大家的意料。L1d的部分跟我們預想的差不多,在一臺P4上耗時為4個周期左右。但L2的結果則出乎意料。大家可能覺得需要14個周期以上,但實際只用了9個周期。這要歸功于處理器先進的處理邏輯,當它使用連續的內存區時,會?預先讀取下一條緩存線的數據。這樣一來,當真正使用下一條線的時候,其實已經早已讀完一半了,于是真正的等待耗時會比L2的訪問時間少很多。
在工作集超過L2的大小之后,預取的效果更明顯了。前面我們說過,主存的訪問需要耗時200個周期以上。但在預取的幫助下,實際耗時保持在9個周期左右。200 vs 9,效果非常不錯。
我們可以觀察到預取的行為,至少可以間接地觀察到。圖3.11中有4條線,它們表示處理不同大小結構時的耗時情況。隨著結構的變大,元素間的距離變大了。圖中4條線對應的元素距離分別是0、56、120和248字節。
圖中最下面的這一條線來自前一個圖,但在這里更像是一條直線。其它三條線的耗時情況比較差。圖中這些線也有比較明顯的三個階段,同時,在小工作集的情況下也有比較大的錯誤(請再次忽略這些錯誤)。在只使用L1d的階段,這些線條基本重合。因為這時候還不需要預取,只需要訪問L1d就行。
在L2階段,三條新加的線基本重合,而且耗時比老的那條線高很多,大約在28個周期左右,差不多就是L2的訪問時間。這表明,從L2到L1d的預取并沒有生效。這是因為,對于最下面的線(NPAD=0),由于結構小,8次循環后才需要訪問一條新緩存線,而上面三條線對應的結構比較大,拿相對最小的NPAD=7來說,光是一次循環就需要訪問一條新線,更不用說更大的NPAD=15和31了。而預取邏輯是無法在每個周期裝載新線的,因此每次循環都需要從L2讀取,我們看到的就是從L2讀取的時延。
更有趣的是工作集超過L2容量后的階段。快看,4條線遠遠地拉開了。元素的大小變成了主角,左右了性能。處理器應能識別每一步(stride)的大小,不去為NPAD=15和31獲取那些實際并不需要的緩存線(參見6.3.1)。元素大小對預取的約束是根源于硬件預取的限制——它無法跨越頁邊界。如果允許預取器跨越頁邊界,而下一頁不存在或無效,那么OS還得去尋找它。這意味著,程序需要遭遇一次并非由它自己產生的頁錯誤,這是完全不能接受的。在NPAD=7或者更大的時候,由于每個元素都至少需要一條緩存線,預取器已經幫不上忙了,它沒有足夠的時間去從內存裝載數據。 另一個導致慢下來的原因是TLB緩存的未命中。TLB是存儲虛實地址映射的緩存,參見第4節。為了保持快速,TLB只有很小的容量。如果有大量頁被反復訪問,超出了TLB緩存容量,就會導致反復地進行地址翻譯,這會耗費大量時間。TLB查找的代價分攤到所有元素上,如果元素越大,那么元素的數量越少,每個元素承擔的那一份就越多。
為了觀察TLB的性能,我們可以進行另兩項測試。第一項:我們還是順序存儲列表中的元素,使NPAD=7,讓每個元素占滿整個cache line,第二項:我們將列表的每個元素存儲在一個單獨的頁上,忽略每個頁沒有使用的部分以用來計算工作集的大小。(這樣做可能不太一致,因為在前面的測試中,我計算了結構體中每個元素沒有使用的部分,從而用來定義NPAD的大小,因此每個元素占滿了整個頁,這樣以來工作集的大小將會有所不同。但是這不是這項測試的重點,預取的低效率多少使其有點不同)。結果表明,第一項測試中,每次列表的迭代都需要一個新的cache line,而且每64個元素就需要一個新的頁。第二項測試中,每次迭代都會在一個新的頁中加載一個新的cache line。
結果見圖3.12。該測試與圖3.11是在同一臺機器上進行的。基于可用RAM空間的有限性,測試設置容量空間大小為2的24次方字節,這就需要1GB的容量將對象放置在分頁上。圖3.12中下方的紅色曲線正好對應了圖3.11中NPAD等于7的曲線。我們看到不同的步長顯示了高速緩存L1d和L2的大小。第二條曲線看上去完全不同,其最重要的特點是當工作容量到達2的13次方字節時開始大幅度增長。這就是TLB緩存溢出的時候。我們能計算出一個64字節大小的元素的TLB緩存有64個輸入。成本不會受頁面錯誤影響,因為程序鎖定了存儲器以防止內存被換出。
可以看出,計算物理地址并把它存儲在TLB中所花費的周期數量級是非常高的。圖3.12的表格顯示了一個極端的例子,但從中可以清楚的得到:TLB緩存效率降低的一個重要因素是大型NPAD值的減緩。由于物理地址必須在緩存行能被L2或主存讀取之前計算出來,地址轉換這個不利因素就增加了內存訪問時間。這一點部分解釋了為什么NPAD等于31時每個列表元素的總花費比理論上的RAM訪問時間要高。
通過查看鏈表元素被修改時測試數據的運行情況,我們可以窺見一些更詳細的預取實現細節。圖3.13顯示了三條曲線。所有情況下元素寬度都為16個字節。第一條曲線“Follow”是熟悉的鏈表走線在這里作為基線。第二條曲線,標記為“Inc”,僅僅在當前元素進入下一個前給其增加thepad[0]成員。第三條曲線,標記為”Addnext0″, 取出下一個元素的thepad[0]鏈表元素并把它添加為當前鏈表元素的thepad[0]成員。
在沒運行時,大家可能會以為”Addnext0″更慢,因為它要做的事情更多——在沒進到下個元素之前就需要裝載它的值。但實際的運行結果令人驚訝——在某些小工作集下,”Addnext0″比”Inc”更快。這是為什么呢?原因在于,系統一般會對下一個元素進行強制性預取。當程序前進到下個元素時,這個元素其實早已被預取在L1d里。因此,只要工作集比L2小,”Addnext0″的性能基本就能與”Follow”測試媲美。
但是,”Addnext0″比”Inc”更快離開L2,這是因為它需要從主存裝載更多的數據。而在工作集達到2?21字節時,”Addnext0″的耗時達到了28個周期,是同期”Follow”14周期的兩倍。這個兩倍也很好解釋。”Addnext0″和”Inc”涉及對內存的修改,因此L2的逐出操作不能簡單地把數據一扔了事,而必須將它們寫入內存。因此FSB的可用帶寬變成了一半,傳輸等量數據的耗時也就變成了原來的兩倍。
決定順序式緩存處理性能的另一個重要因素是緩存容量。雖然這一點比較明顯,但還是值得一說。圖3.14展示了128字節長元素的測試結果(64位機,NPAD=15)。這次我們比較三臺不同計算機的曲線,兩臺P4,一臺Core 2。兩臺P4的區別是緩存容量不同,一臺是32k的L1d和1M的L2,一臺是16K的L1d、512k的L2和2M的L3。Core 2那臺則是32k的L1d和4M的L2。
圖中最有趣的地方,并不是Core 2如何大勝兩臺P4,而是工作集開始增長到連末級緩存也放不下、需要主存熱情參與之后的部分。
與我們預計的相似,最末級緩存越大,曲線停留在L2訪問耗時區的時間越長。在220字節的工作集時,第二臺P4(更老一些)比第一臺P4要快上一倍,這要完全歸功于更大的末級緩存。而Core 2拜它巨大的4M L2所賜,表現更為卓越。
對于隨機的工作負荷而言,可能沒有這么驚人的效果,但是,如果我們能將工作負荷進行一些裁剪,讓它匹配末級緩存的容量,就完全可以得到非常大的性能提升。也是由于這個原因,有時候我們需要多花一些錢,買一個擁有更大緩存的處理器。
單線程隨機訪問模式的測量
前面我們已經看到,處理器能夠利用L1d到L2之間的預取消除訪問主存、甚至是訪問L2的時延。
但是,如果換成隨機訪問或者不可預測的訪問,情況就大不相同了。圖3.15比較了順序讀取與隨機讀取的耗時情況。
換成隨機之后,處理器無法再有效地預取數據,只有少數情況下靠運氣剛好碰到先后訪問的兩個元素挨在一起的情形。
圖3.15中有兩個需要關注的地方。首先,在大的工作集下需要非常多的周期。這臺機器訪問主存的時間大約為200-300個周期,但圖中的耗時甚至超過了450個周期。我們前面已經觀察到過類似現象(對比圖3.11)。這說明,處理器的自動預取在這里起到了反效果。
其次,代表隨機訪問的曲線在各個階段不像順序訪問那樣保持平坦,而是不斷攀升。為了解釋這個問題,我們測量了程序在不同工作集下對L2的訪問情況。結果如圖3.16和表3.2。
從圖中可以看出,當工作集大小超過L2時,未命中率(L2未命中次數/L2訪問次數)開始上升。整條曲線的走向與圖3.15有些相似: 先急速爬升,隨后緩緩下滑,最后再度爬升。它與耗時圖有緊密的關聯。L2未命中率會一直爬升到100%為止。只要工作集足夠大(并且內存也足夠大),就可以將緩存線位于L2內或處于裝載過程中的可能性降到非常低。
緩存未命中率的攀升已經可以解釋一部分的開銷。除此以外,還有一個因素。觀察表3.2的L2/#Iter列,可以看到每個循環對L2的使用次數在增長。由于工作集每次為上一次的兩倍,如果沒有緩存的話,內存的訪問次數也將是上一次的兩倍。在按順序訪問時,由于緩存的幫助及完美的預見性,對L2使用的增長比較平緩,完全取決于工作集的增長速度。
而換成隨機訪問后,單位耗時的增長超過了工作集的增長,根源是TLB未命中率的上升。圖3.17描繪的是NPAD=7時隨機訪問的耗時情況。這一次,我們修改了隨機訪問的方式。正常情況下是把整個列表作為一個塊進行隨機(以∞表示),而其它11條線則是在小一些的塊里進行隨機。例如,標簽為’60′的線表示以60頁(245760字節)為單位進行隨機。先遍歷完這個塊里的所有元素,再訪問另一個塊。這樣一來,可以保證任意時刻使用的TLB條目數都是有限的。 NPAD=7對應于64字節,正好等于緩存線的長度。由于元素順序隨機,硬件預取不可能有任何效果,特別是在元素較多的情況下。這意味著,分塊隨機時的L2未命中率與整個列表隨機時的未命中率沒有本質的差別。隨著塊的增大,曲線逐漸逼近整個列表隨機對應的曲線。這說明,在這個測試里,性能受到TLB命中率的影響很大,如果我們能提高TLB命中率,就能大幅度地提升性能(在后面的一個例子里,性能提升了38%之多)。
總結
以上是生活随笔為你收集整理的产生线程安全的原因(2)(操作系统)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大病互助金是什么
- 下一篇: 产生线程安全的原因(3)(操作系统)