記憶體分配的隱藏成本

光光頭去打醬油發表於2014-12-29

理解記憶體分配的成本很重要,但測量這一成本又需要很多技巧。通過定時呼叫new[]和delete[]來測量似乎是個不錯的方法。然而對於大型緩衝區,定時器可能會遺漏99%以上的真實成本,這些隱性成本可能比我預期的更大。

我們來看看更復雜的測量,事實證明,有些成本可能轉移到另外一個程式,儘管測量似乎很合理,但看不到任何結果。本文(特指在Windows環境)我將解釋這些隱性成本是什麼,他們如何隱藏,如何衡量它們,應該怎麼做。

快速測試

下面這些釋放和重新分配記憶體的成本,哪些需要你完整的記錄,而不是反覆使用它們?

  1. 重新分配和釋放記憶體的時間。
  2. 使用記憶體的時間。
  3. 系統程式消耗的CPU時間。
  4. 以上全部。

劇透警告,答案是#4。對於足夠大的記憶體塊來說,重新分配和釋放的時間和分配處理相關操作的CPU開銷來說,只是一部分。

我承認我喜歡大記憶體

在這篇文章中我只關注大記憶體的分配—大到可以分配給作業系統。為了專心的關注作業系統分配大記憶體的問題,我們就忽略那些堆碎片等細小的問題了。這些問題是真實存在的,我遇到過好幾次了,修復這些問題很容易就能提升效能,但是我不想在這篇文章中把所有的記憶體分配問題都列舉出來。

測量分配和釋放成本的方法

測量分配和釋放的成本似乎很簡單。在一個迴圈中,測量不同大小的記憶體塊new[]/delete[]操作需要多長時間。在我的測試中,記憶體大小從8MB至32MB,new[]/delete[]操作的成本平均大概7.5μs(微秒),其中約5μs花在了分配,約2.5μs花在了釋放。在測試中發現分配的大小似乎並沒有明顯的影響結果。

釋放將變得緩慢

分配了記憶體卻不使用它有點太假了,所以接下來在釋放記憶體之前,我們要將它寫進整個記憶體塊中。停下來想想,這會對分配和釋放記憶體的成本產生什麼影響呢。我們拭目以待。

這種情況下分配記憶體的成本略有上升,大概是因為寫記憶體將會從CPU快取中重新整理各種有用的資料,使得隨後的分配稍微慢了些,這增加8μs的分配成本。

釋放記憶體的成本就變的更貴了。寫記憶體之前釋放成本大概是每MB75μs,相比之前釋放未使用的記憶體成本約2.5μs,是一個巨大的提升。這意味著釋放32MB使用過的記憶體要用2400μs(2.4ms)!

記憶體,按需分配

這看似奇怪的行為背後,是因為Windows作業系統的懶惰。對於大記憶體的分配(1MB或以上)Windows記憶體堆(new/new[], malloc/HeapAlloc)會使用系統的虛擬分配(VirtualAlloc)功能(with MEM_COMMIT | MEM_RESERVE)來請求記憶體。這將保留地址空間和分配提交指令,但是實際上並不提交任何page。由虛擬分配(VirtualAlloc)返回的指標基本上就是個承諾—承諾當page被訪問時,一定會有記憶體給你。就像文件中說的“實際上實體記憶體頁是沒有分配的,除非虛擬地址被實際訪問了”這是一件好事,因為應用程式申請的記憶體可能比實際需要的記憶體多,這樣能減少浪費,Linux也有類似的機制。

所以,分配釋放那些沒有被操作命中的記憶體成本是非常小的。但是如果記憶體被命中了記憶體頁也就不連續了,釋放這些記憶體實際上要刪除它們。從程式地址空間中刪除這些記憶體頁需要每MB75μs。

Faulty towers

如果從程式地址空間刪除這些記憶體頁需要75μs每MB,那將記憶體頁放進地址空間也需要一定的時間。這種成本很難去測量,因為只有在第一次訪問記憶體頁真正請求記憶體的時候才會發生。最簡單的測量方法就是測量下寫入新分配的記憶體塊需要多長時間,然後和寫入之前分配的記憶體塊時間相比較。

這種方法比較冒險。測量完可能得到不同的結果。如果記憶體塊太小,那測量的可能是快取的效果。除此之外,還可能受到TLB( Translation Lookaside Buffer)的影響。如果沒有足夠的記憶體那麼記憶體頁會在工作區中被刪除,然後在需要的時候被替換回來。還有其他的一些程式也很容易影響到結果。像CPU執行時的頻率變化就會干涉到結果。

但是,我盡了我最大的努力來避免各種干擾。我的CPU三級快取(L3)是6MB的,所以我測試從8MB至32MB,儘量避免快取的影響。我的記憶體很大,也關閉了很多無關的程式來避免影響到結果。我測試了很多次。我用了高效能電源模式,還在後臺跑了一個執行緒來保證CPU頻率一直保持在一個較高的水平。我還將ETW (xperf) 配置記錄下來了,檢查它們來找出錯誤源。

結果很清楚。不連續的記憶體頁第一次使用的成本最小大概175μs每MB。某些情況下,成本會變得很高,至於原因還不太清楚,但是對於分配8MB以上的記憶體,第一次使用的時候你可以假設最小的成本就是約175μs每MB。

軟頁錯誤,硬頁錯誤

軟頁錯誤和硬頁錯誤之間是有區別的。軟頁錯誤是記憶體缺頁造成的。新分配的記憶體頁第一次被引用,或在備用列表中被發現有缺陷—可能從工作集中被修整過,或者對映檔案在磁碟快取上。軟頁錯誤成本雖然昂貴,但是在這種情況下,還是算相當便宜的。

硬頁錯誤的發生跟磁碟有關,也許記憶體頁在記憶體對映檔案中或者從交換檔案中恢復資料。這些成本是非常昂貴的,大家都知道磁碟讀寫速度是非常慢的。硬頁錯誤很容易就導致成本超過1s每MB,但是那些是硬錯誤,本文主要關注那些很便宜的軟頁錯誤。

(想了解更多 http://en.wikipedia.org/wiki/Page_fault)

歸零

但是,等等,還有一個我們至今未提的記憶體分配成本。

出於安全考慮,新對映的記憶體頁都將歸零,否則會在程式之間洩露資訊。所有的現代作業系統都會有這個操作。記憶體頁歸零操作的成本不是很貴,但也不便宜。

Windows作業系統試圖保證一個可用的歸零記憶體頁池,可以快速分配給那些發生記憶體故障的程式。但是這個記憶體頁池需要被釋放的記憶體頁和歸零記憶體頁來補充。這意味著系統程式有一個低優先順序的執行緒來將那些釋放的記憶體也歸零。如果它是一直持續執行的,那麼意味著所有被釋放的記憶體歸零操作都是在你的程式程式之外完成的,這部分成本被隱藏了,你無法測量到。

想要了解更多關於記憶體頁生命週期的資訊可以看看這個網頁

但是沒有什麼可以逃過ETW (xperf)的“火眼金睛”。CPU使用率被曲線圖精確的展示出來了,紅色的是測試程式,綠色的是系統程式。每個測試迴圈結束時,測試程式釋放了記憶體,你可以看到系統程式的“生命軌跡”。

這意味著只有一種方法來測量那些微小的記憶體分配成本,用WTP同時記錄下你的程式和系統程式的“軌跡”。

如果歸零執行緒無法跟上節奏,那記憶體頁歸零操作可能在你的程式中進行,不過在KeZeroMemory中可以看得出來,這種情況一般發生在那些

CPU計算能力較弱的機器上。

CPU使用率(取樣)資料會告訴你歸零執行緒在幹什麼。然後你會發現執行緒8一直都是歸零執行緒,可以用CPU使用率(精確)資料來測量它每次喚醒工作的時長。被命中然後釋放掉時長大概是150μs每MB。

總成本

總之,簡單的測量分配釋放大記憶體塊的成本可能會得到這樣一個結果:分配/釋放這樣一組操作只需要約7.5μs。事實上對於較大記憶體的分配卻有三個部分的成本。就像表格所展示的,這些加起來大約400μs每MB。每幀分配8MB緩衝區(可能用來存放1080p RGBA 影象)很容易就消耗掉每幀CPU時間3.2ms(μs)。像這樣一些成本隱藏在系統程式中,它們可能對那些多核計算機效能沒有影響,但是對於雙核的機器還是有一定影響的。

值得一提的是,這些數字大多都是取的最低值。尤其是在記憶體頁故障的時候,有時候成本會高的多,至於原因還不是很清楚。

實時監控歸零記憶體頁

想要準確可靠的知道你的程式在幹什麼,效能如何,使用ETW跟蹤記錄是個不錯的辦法。我記錄了很多我也學到了很多。最近我養成了一個習慣,就是在我的程式中找KiPageFault ,觀察歸零執行緒是否有大量活動。

但是這比較枯燥而且耗費時間。實時監控歸零執行緒的活動非常有幫助。這個執行緒有活動說明有大量新的記憶體被分配,我們很容易就能聯想到。

事實證明,這種監控是很容易。

我提供的資訊使用到了一些未證實的Windows作業系統的細節,不同版本可能會有所不同。這些資訊僅用於診斷目的。現在,這些所有權歸我,如果你釋出的產品中使用了這些資訊,我會叫上Raymond Chen 組成一個團隊來維權,並且會在你的編譯器裡打亂這些位元組碼。

我觀察到(見上文免責宣告)在我的測試中,歸零執行緒的ID總是8。因此我們可以很容易的監視歸零執行緒的活動水平:

  1. 用OpenThread 來處理8號執行緒
  2. 用QueryThreadCycleTime 來獲取該執行緒的生命週期
  3. 在迴圈中延遲1000ms並在最後一秒中列印出歸零生命週期的消耗

就這麼簡單。程式要以管理員許可權執行,這明顯像是一個危險的攻擊程式,但是它能幫我們識別出那些讓系統頻繁重新分配記憶體的程式碼。如果一執行你的程式碼,歸零執行緒就變的很忙碌。那麼你的程式可能需要跟蹤一下,看看有沒有KiPageFault。

測試步驟

所有的測試都是在我的電腦上完成的,電腦配置為四核八執行緒Core(TM) i7-2720QM CPU@ 2.20 GHz (可睿頻至 3.3 GHz)
8G記憶體,作業系統為Windows 7 SP1。對比測試在Windows 8.1 上完成的,結果是相似的。

粗糙的測試程式碼

Linux

我在Linux中也做了些對比測試,但是沒法精確的測量到進出記憶體對映頁的成本。我看到核心歸零記憶體並在第一次寫後對映——執行 perf top 命令,可以看到clear_page_c_e 或者類似的方法。搜尋了一下發現之前開發人員已經注意到clear_page_c_e的成本了。Linux似乎是按需清理記憶體頁,而不是有一個專門的執行緒, 這兩種方式各有優缺點。

相關文章

如果你想了解更多關於怎麼使用ETW來深入的研究Windows效能這類資訊,那麼我推薦xperf系列文章,其中包括各種教程和文件。尤其是Wintellect Now訓練視訊,可能這是我建立的最好的資源了,可以通過這免費的觀看。

相關文章