1. 程式人生 > >大頁記憶體(HugePages)在通用程式優化中的應用

大頁記憶體(HugePages)在通用程式優化中的應用

在介紹之前需要強調一點,大頁記憶體也有適用範圍,程式耗費記憶體很小或者程式的訪存區域性性很好,大頁記憶體很難獲得性能提升。所以,如果你面臨的程式優化問題有上述兩個特點,請不要考慮大頁記憶體。後面會詳細解釋為啥具有上述兩個特點的程式大頁記憶體無效。

  1. 背景

近期一直在公司從事聽歌識曲專案的開發,詳細內容可參考:基於指紋的音樂檢索,目前已上線到搜狗語音雲開放平臺。在開發的過程中,遇到一個很嚴重的效能問題,單執行緒測試的時候效能還能達到要求,但是在多執行緒進行壓力測試的時候,演算法最耗時的部分突然變慢了好幾倍!後來經過仔細除錯,發現最影響效能的居然是一個編譯選項-pg,去掉它之後效能會好很多,但是還是會比單執行緒的效能慢2倍左右,這就會導致系統的實時率達到1.0以上,響應能力嚴重下降。

通過更加仔細的分析,我們發現系統最耗時的部分是訪問指紋庫的過程,但是這部分根本就沒有優化餘地,只能換用記憶體頻寬更高的機器。換用記憶體頻寬更高的機器確實帶來了不少效能的提升,但是還是無法達到要求。就在山重水盡的情況下,無意中看到MSRA的洪春濤博士在微博中提到他們用大頁記憶體對一個隨機陣列的訪問問題進行優化獲得了很好的效能提升。然後就向他求助,終於通過大頁記憶體這種方法使系統性能進一步提升,實時率也降到了0.4左右。圓滿達成目標!

  1. 基於指紋的音樂檢索簡介

檢索過程其實和搜尋引擎一樣,音樂指紋就和搜尋引擎中的關鍵詞等價,指紋庫就等價於搜尋引擎的後臺網頁庫。指紋庫的構造和搜尋引擎的網頁庫也是一樣,採用倒排索引形式。如下圖:

這裡寫圖片描述
圖1 基於指紋的倒排索引表

只不過指紋都是一個int型的整數(圖示只佔用了24位),包含的資訊太少,所以需要提取很多個指紋完成一次匹配,大約是每秒幾千個的樣子。每獲得一個指紋都需要訪問指紋庫獲得對應的倒排列表,然後再根據音樂id構造一個正排列表,用來分析哪首音樂匹配上,如下圖:

這裡寫圖片描述
圖2 統計匹配的相似度

最終的結果就是排序結果最高的音樂。

目前指紋庫大約60G,是對25w首歌提取指紋的結果。每一個指紋對應的倒排列表長度不固定,但是有上限7500。正排列表的音樂個數也是25w,每一首音樂對應的最長時間差個數為8192。單次檢索的時候會生成大約1000個左右的指紋(甚至更多)。

通過上面的介紹,可以看出基於指紋的音樂檢索(聽歌識曲)共有三部分:1.提取指紋;2.訪問指紋庫;3.排序時間差。多執行緒情況下,這三部分的時間耗費比例大約是:1%、80%和19%,也即大部分時間都耗費在查詢指紋庫的操作上。更麻煩的一點是,指紋庫的訪問全部是亂序訪問,沒有一點區域性性可言,所以cache一直在缺失,常規的優化方法都無效,只能換成記憶體頻寬更高的伺服器。

不過正是由於上述的特點—耗費記憶體巨大(100G左右)、亂序訪存且訪存是瓶頸,導致大頁記憶體特別適合來優化上面遇到的效能瓶頸問題。

  1. 原理

大頁記憶體的原理涉及到作業系統的虛擬地址到實體地址的轉換過程。作業系統為了能同時執行多個程序,會為每個程序提供一個虛擬的程序空間,在32位作業系統上,程序空間大小為4G,64位系統為2^64(實際可能小於這個值)。在很長一段時間內,我對此都非常疑惑,這樣不就會導致多個程序訪存的衝突嗎,比如,兩個程序都訪問地址0x00000010的時候。事實上,每個程序的程序空間都是虛擬的,這和實體地址還不一樣。兩個進行訪問相同的虛擬地址,但是轉換到實體地址之後是不同的。這個轉換就通過頁表來實現,涉及的知識是作業系統的分頁儲存管理。

分頁儲存管理將程序的虛擬地址空間,分成若干個頁,併為各頁加以編號。相應地,實體記憶體空間也分成若干個塊,同樣加以編號。頁和塊的大小相同。假設每一頁的大小是4K,則32位系統中分頁地址結構為:
這裡寫圖片描述

為了保證程序能在記憶體中找到虛擬頁對應的實際物理塊,需要為每個程序維護一個映像表,即頁表。頁表記錄了每一個虛擬頁在記憶體中對應的物理塊號,如圖三。在配置好了頁表後,程序執行時,通過查詢該表,即可找到每頁在記憶體中的物理塊號。

在作業系統中設定有一個頁表暫存器,其中存放了頁表在記憶體的始址和頁表的長度。程序未執行時,頁表的始址和頁表長度放在本程序的PCB中;當排程程式排程該程序時,才將這兩個資料裝入頁表暫存器。

當程序要訪問某個虛擬地址中的資料時,分頁地址變換機構會自動地將有效地址(相對地址)分為頁號和頁內地址兩部分,再以頁號為索引去檢索頁表,查詢操作由硬體執行。若給定的頁號沒有超出頁表長度,則將頁表始址與頁號和頁表項長度的乘積相加,得到該表項在頁表中的位置,於是可以從中得到該頁的物理塊地址,將之裝入實體地址暫存器中。與此同時,再將有效地址暫存器中的頁內地址送入實體地址暫存器的塊內地址欄位中。這樣便完成了從虛擬地址到實體地址的變換。

這裡寫圖片描述
圖3 頁表的作用

由於頁表是存放在記憶體中的,這使CPU在每存取一個數據時,都要兩次訪問記憶體。第一次時訪問記憶體中的頁表,從中找到指定頁的物理塊號,再將塊號與頁內偏移拼接,以形成實體地址。第二次訪問記憶體時,才是從第一次所得地址中獲得所需資料。因此,採用這種方式將使計算機的處理速度降低近1/2。

為了提高地址變換速度,可在地址變換機構中,增設一個具有並行查詢能力的特殊快取記憶體,也即快表(TLB),用以存放當前訪問的那些頁表項。具有快表的地址變換機構如圖四所示。由於成本的關係,快表不可能做得很大,通常只存放16~512個頁表項。

上述地址變換機構對中小程式來說執行非常好,快表的命中率非常高,所以不會帶來多少效能損失,但是當程式耗費的記憶體很大,而且快表命中率不高時,那麼問題來了。

這裡寫圖片描述
圖4 具有快表的地址變換機構

  1. 小頁的困境

    現代的計算機系統,都支援非常大的虛擬地址空間(2^32~2^64)。在這樣的環境下,頁表就變得非常龐大。例如,假設頁大小為4K,對佔用40G記憶體的程式來說,頁表大小為10M,而且還要求空間是連續的。為了解決空間連續問題,可以引入二級或者三級頁表。但是這更加影響效能,因為如果快表缺失,訪問頁表的次數由兩次變為三次或者四次。由於程式可以訪問的記憶體空間很大,如果程式的訪存區域性性不好,則會導致快表一直缺失,從而嚴重影響效能。

    此外,由於頁表項有10M之多,而快表只能快取幾百頁,即使程式的訪存效能很好,在大記憶體耗費情況下,快表缺失的概率也很大。那麼,有什麼好的方法解決快表缺失嗎?大頁記憶體!假設我們將頁大小變為1G,40G記憶體的頁表項也只有40,快表完全不會缺失!即使缺失,由於表項很少,可以採用一級頁表,缺失只會導致兩次訪存。這就是大頁記憶體可以優化程式效能的根本原因—快表幾乎不缺失!

    在前面我們提到如果要優化的程式耗費記憶體很少,或者訪存區域性性很好,大頁記憶體的優化效果就會很不明顯,現在我們應該明白其中緣由。如果程式耗費記憶體很少,比如只有幾M,則頁表項也很少,快表很有可能會完全快取,即使缺失也可以通過一級頁表替換。如果程式訪存區域性性也很好,那麼在一段時間內,程式都訪問相鄰的記憶體,快表缺失的概率也很小。所以上述兩種情況下,快表很難缺失所以大頁記憶體就體現不出優勢來。

  2. 大頁記憶體的配置和使用

    網上很多資料在介紹大頁記憶體的時候都會伴隨它在Oracle資料庫中的使用,這會讓人產生一種錯覺:大頁記憶體只能在Oracle資料庫中使用。通過上面的分析,我們可以知道,其實大頁記憶體是一種很通用的優化技術。它的優化方法就是避免快表缺失。那麼怎麼具體應用呢,下面詳細介紹使用的步驟。

  3. 安裝libhugetlbfs庫

    libhugetlbfs庫實現了大頁記憶體的訪問。安裝可以通過apt-get或者yum命令完成,如果系統沒有該命令,還可以從官網下載。

  4. 配置grub啟動檔案

    這一步很關鍵,決定著你分配的每個大頁的大小和多少大頁。具體操作是編輯/etc/grub.conf檔案,如圖五所示。

圖5 grub.conf啟動指令碼

具體就是在kernel選項的最後新增幾個啟動引數:transparent_hugepage=never default_hugepagesz=1G hugepagesz=1Ghugepages=123。這四個引數中,最重要的是後兩個,hugepagesz用來設定每頁的大小,我們將其設定為1G,其他可選的配置有4K,2M(其中2M是預設)。如果作業系統版本太低的情況下,可能會導致1G的頁設定失敗,所以設定失敗請檢視自己作業系統的版本。hugepages用來設定多少頁大頁記憶體,我們的系統記憶體是128G,現在分配123G用來專門服務大頁。這裡需要注意,分配完的大頁對常規程式來說是不可見的,例如我們的系統還剩餘5G的普通記憶體,這時我如果按照常規方法啟動一個耗費10G的程式就會失敗。修改完grub.conf後,重啟系統。然後執行命令cat /proc/meminfo|grep Huge命令檢視大頁設定是否生效,如果生效,將會顯示如下內容:

圖6 當前的大頁耗費情況

我們需要關注其中的四個值,HugePages_Total表示目前總共有多少個大頁,HugePages_Free表示程式執行起來之後還剩餘多少個大頁,HugePages_Rsvd表示系統當前總共保留的HugePages數目,更具體點就是指程式已經向系統申請,但是由於程式還沒有實質的HugePages讀寫操作,因此係統尚未實際分配給程式的HugePages數目。Hugepagesize表示每個大頁的大小,在此為1GB。

   我們在實驗中發現一個問題,Free的值和Rsvd的值可能和字面意思不太一樣。如果一開始我們申請的大頁不足以啟動程式,系統就會提示如下錯誤:

ibhugetlbfs:WARNING: New heap segment map at 0x40000000 failed: Cannot allocate memory

此時,再次檢視上述四個值會發現這樣的情況:HugePages_Free等於a,HugePages_Rsvd等於a。這讓人感到很奇怪,明明還有剩餘的大頁,但是系統報錯,提示大頁分配失敗。經過多次嘗試,我們認為Free中應該是包括Rsvd的大頁的,所以當Free等於Rsvd的時候其實已經沒有可用的大頁了。Free減Rsvd才是真正能夠再次分配的大頁。例如,在圖六中還有16個大頁可以被分配。

具體應該分配多少個大頁合適,這個需要多次嘗試,我們得到的一個經驗是:子執行緒對大頁的使用很浪費,最好是所有的空間都在主執行緒分配,然後再分配給各個子執行緒,這樣會顯著減少大頁浪費。

  1. mount

執行mount,將大頁記憶體映像到一個空目錄。可以執行下述命令:
[plain] view plain copy
在CODE上檢視程式碼片派生到我的程式碼片
if [ ! -d /search/music/libhugetlbfs ]; then
mkdir /search/music/libhugetlbfs
fi
mount -t hugetlbfs hugetlbfs /search/music/libhugetlbfs
4. 執行應用程式

為了能啟用大頁,不能按照常規的方法啟動應用程式,需要按照下面的格式啟動:

HUGETLB_MORECORE=yesLD_PRELOAD=libhugetlbfs.so ./your_program

這種方法會載入libhugetlbfs庫,用來替換標準庫。具體的操作就是替換標準的malloc為大頁的malloc。此時,程式申請記憶體就是大頁記憶體了。

按照上述四個步驟即可啟用大頁記憶體,所以啟用大頁還是很容易的。

  1. 大頁記憶體的優化效果

如果你的應用程式亂序訪存很嚴重,那麼大頁記憶體會帶來比較大的收益,正好我們現在做的聽歌識曲就是這樣的應用,所以優化效果很明顯,下面是曲庫為25w時,啟用大頁和不啟用大頁的程式效能。

可以看出,啟用大頁記憶體之後,程式的訪問時間顯著下降,效能提升接近50%,達到了效能要求。

  1. 大頁記憶體的使用場景

任何優化手段都有它適用的範圍,大頁記憶體也不例外。前面我們一直強調,只有耗費的記憶體巨大、訪存隨機而且訪存是瓶頸的程式大頁記憶體才會帶來很明顯的效能提升。在我們的聽歌識曲系統中,耗費的記憶體接近100G,而且記憶體訪問都是亂序訪問,所以才帶來明顯的效能提升。網上的例子一直在用Oracle資料庫作為例子不是沒有道理的,這是因為Oracle資料庫耗費的記憶體也很巨大,而且資料庫的增刪查改也缺乏區域性性。資料庫背後的增刪查改基本上是對B樹進行操作,樹的操作一般缺少區域性性。

什麼樣的程式區域性性較差呢?我個人認為採用雜湊和樹策略實現的程式往往具有較差的訪存區域性性,這時如果程式效能不好可以嘗試大頁記憶體。相反,單純的陣列遍歷或者圖的廣度遍歷等操作,具有很好的訪存區域性性,採用大頁記憶體很難獲得性能提升。本人曾經嘗試在搜狗語音識別解碼器上啟用大頁記憶體,希望可以獲得性能提升,但是效果令人失望,沒有提升反而導致效能降低。這是因為語音識別解碼器從本質上來講就是一個圖的廣搜,具有很好的訪存區域性性,而且訪存不是效能瓶頸,這時採用大頁記憶體可能會帶來其他開銷,導致效能下降。

  1. 總結

本部落格以聽歌識曲的例子詳細介紹了大頁記憶體的原理和使用方法。由於大資料的興盛,目前應用程式處理的資料量越來越大,而且資料的訪問越來越不規整,這些條件給大頁記憶體的使用帶來了可能。所以,如果你的程式跑得慢,而且滿足大頁記憶體的使用條件,那就嘗試一下吧,反正很簡單又沒損失,萬一能帶來不錯的效果呢。