原文轉載自:http://blog.csdn.net/yutianzuijin/article/details/41912871
今天給大家介紹一種比較新奇的程序性能優化方法—大頁內存(HugePages),簡單來說就是通過增大操作系統頁的大小來減小頁表,從而避免快表 缺失。這方面的資料比較貧乏,而且網上絕大多數資料都是介紹它在Oracle數據庫中的應用,這會讓人產生一種錯覺:這種技術只能在Oracle數據庫中 應用。但其實,大頁內存可以算是一種非常通用的優化技術,應用范圍很廣,針對不同的應用程序,最多可能會帶來50%的性能提升,優化效果還是非常明顯的。 在本博客中,將通過一個具體的例子來介紹大頁內存的使用方法。
在介紹之前需要強調一點,大頁內存也有適用范圍,程序耗費內存很小或者程序的訪存局部性很好,大頁內存很難獲得性能提升。所以,如果你面臨的程序優化問題有上述兩個特點,請不要考慮大頁內存。后面會詳細解釋為啥具有上述兩個特點的程序大頁內存無效。
1. 背景
近期一直在公司從事聽歌識曲項目的開發,詳細內容可參考:基於指紋的音樂檢索,目前已上線到搜狗語音雲開放平台。 在開發的過程中,遇到一個很嚴重的性能問題,單線程測試的時候性能還能達到要求,但是在多線程進行壓力測試的時候,算法最耗時的部分突然變慢了好幾倍!后 來經過仔細調試,發現最影響性能的居然是一個編譯選項-pg,去掉它之后性能會好很多,但是還是會比單線程的性能慢2倍左右,這就會導致系統的實時率達到 1.0以上,響應能力嚴重下降。
通過更加仔細的分析,我們發現系統最耗時的部分是訪問指紋庫的過程,但是這部分根本就沒有優化余地,只能換用內存帶寬更高的機器。換用內存帶寬更高的機器 確實帶來了不少性能的提升,但是還是無法達到要求。就在山重水盡的情況下,無意中看到MSRA的洪春濤博士在微博中提到他們用大頁內存對一個隨機數組的訪問問題進行優化獲得了很好的性能提升。然后就向他求助,終於通過大頁內存這種方法使系統性能進一步提升,實時率也降到了0.4左右。圓滿達成目標!
2. 基於指紋的音樂檢索簡介
檢索過程其實和搜索引擎一樣,音樂指紋就和搜索引擎中的關鍵詞等價,指紋庫就等價於搜索引擎的后台網頁庫。指紋庫的構造和搜索引擎的網頁庫也是一樣,采用倒排索引形式。如下圖:
圖1 基於指紋的倒排索引表
只不過指紋都是一個int型的整數(圖示只占用了24位),包含的信息太少,所以需要提取很多個指紋完成一次匹配,大約是每秒幾千個的樣子。每獲得一個指紋都需要訪問指紋庫獲得對應的倒排列表,然后再根據音樂id構造一個正排列表,用來分析哪首音樂匹配上,如下圖:

圖2 統計匹配的相似度
最終的結果就是排序結果最高的音樂。
目前指紋庫大約60G,是對25w首歌提取指紋的結果。每一個指紋對應的倒排列表長度不固定,但是有上限7500。正排列表的音樂個數也是25w,每一首音樂對應的最長時間差個數為8192。單次檢索的時候會生成大約1000個左右的指紋(甚至更多)。
通過上面的介紹,可以看出基於指紋的音樂檢索(聽歌識曲)共有三部分:1.提取指紋;2.訪問指紋庫;3.排序時間差。多線程情況下,這三部分的時 間耗費比例大約是:1%、80%和19%,也即大部分時間都耗費在查找指紋庫的操作上。更麻煩的一點是,指紋庫的訪問全部是亂序訪問,沒有一點局部性可 言,所以cache一直在缺失,常規的優化方法都無效,只能換成內存帶寬更高的服務器。
不過正是由於上述的特點—耗費內存巨大(100G左右)、亂序訪存且訪存是瓶頸,導致大頁內存特別適合來優化上面遇到的性能瓶頸問題。
3. 原理
大頁內存的原理涉及到操作系統的虛擬地址到物理地址的轉換過程。操作系統為了能同時運行多個進程,會為每個進程提供一個虛擬的進程空間,在32位操 作系統上,進程空間大小為4G,64位系統為2^64(實際可能小於這個值)。在很長一段時間內,我對此都非常疑惑,這樣不就會導致多個進程訪存的沖突 嗎,比如,兩個進程都訪問地址0x00000010的時候。事實上,每個進程的進程空間都是虛擬的,這和物理地址還不一樣。兩個進行訪問相同的虛擬地址, 但是轉換到物理地址之后是不同的。這個轉換就通過頁表來實現,涉及的知識是操作系統的分頁存儲管理。
分頁存儲管理將進程的虛擬地址空間,分成若干個頁,並為各頁加以編號。相應地,物理內存空間也分成若干個塊,同樣加以編號。頁和塊的大小相同。假設每一頁的大小是4K,則32位系統中分頁地址結構為:

為了保證進程能在內存中找到虛擬頁對應的實際物理塊,需要為每個進程維護一個映像表,即頁表。頁表記錄了每一個虛擬頁在內存中對應的物理塊號,如圖三。在配置好了頁表后,進程執行時,通過查找該表,即可找到每頁在內存中的物理塊號。
在操作系統中設置有一個頁表寄存器,其中存放了頁表在內存的始址和頁表的長度。進程未執行時,頁表的始址和頁表長度放在本進程的PCB中;當調度程序調度該進程時,才將這兩個數據裝入頁表寄存器。
當進程要訪問某個虛擬地址中的數據時,分頁地址變換機構會自動地將有效地址(相對地址)分為頁號和頁內地址兩部分,再以頁號為索引去檢索頁表,查找 操作由硬件執行。若給定的頁號沒有超出頁表長度,則將頁表始址與頁號和頁表項長度的乘積相加,得到該表項在頁表中的位置,於是可以從中得到該頁的物理塊地 址,將之裝入物理地址寄存器中。與此同時,再將有效地址寄存器中的頁內地址送入物理地址寄存器的塊內地址字段中。這樣便完成了從虛擬地址到物理地址的變 換。

圖3 頁表的作用
由於頁表是存放在內存中的,這使CPU在每存取一個數據時,都要兩次訪問內存。第一次時訪問內存中的頁表,從中找到指定頁的物理塊號,再將塊號與頁 內偏移拼接,以形成物理地址。第二次訪問內存時,才是從第一次所得地址中獲得所需數據。因此,采用這種方式將使計算機的處理速度降低近1/2。
為了提高地址變換速度,可在地址變換機構中,增設一個具有並行查找能力的特殊高速緩存,也即快表(TLB),用以存放當前訪問的那些頁表項。具有快表的地址變換機構如圖四所示。由於成本的關系,快表不可能做得很大,通常只存放16~512個頁表項。
上述地址變換機構對中小程序來說運行非常好,快表的命中率非常高,所以不會帶來多少性能損失,但是當程序耗費的內存很大,而且快表命中率不高時,那么問題來了。

圖4 具有快表的地址變換機構
4. 小頁的困境
現代的計算機系統,都支持非常大的虛擬地址空間(2^32~2^64)。在這樣的環境下,頁表就變得非常龐大。例如,假設頁大小為4K,對占用40G內存 的程序來說,頁表大小為10M,而且還要求空間是連續的。為了解決空間連續問題,可以引入二級或者三級頁表。但是這更加影響性能,因為如果快表缺失,訪問 頁表的次數由兩次變為三次或者四次。由於程序可以訪問的內存空間很大,如果程序的訪存局部性不好,則會導致快表一直缺失,從而嚴重影響性能。
此外,由於頁表項有10M之多,而快表只能緩存幾百頁,即使程序的訪存性能很好,在大內存耗費情況下,快表缺失的概率也很大。那么,有什么好的方法解決快 表缺失嗎?大頁內存!假設我們將頁大小變為1G,40G內存的頁表項也只有40,快表完全不會缺失!即使缺失,由於表項很少,可以采用一級頁表,缺失只會 導致兩次訪存。這就是大頁內存可以優化程序性能的根本原因—快表幾乎不缺失!
在前面我們提到如果要優化的程序耗費內存很少,或者訪存局部性很好,大頁內存的優化效果就會很不明顯,現在我們應該明白其中緣由。如果程序耗費內存很少, 比如只有幾M,則頁表項也很少,快表很有可能會完全緩存,即使缺失也可以通過一級頁表替換。如果程序訪存局部性也很好,那么在一段時間內,程序都訪問相鄰 的內存,快表缺失的概率也很小。所以上述兩種情況下,快表很難缺失所以大頁內存就體現不出優勢來。
5. 大頁內存的配置和使用
網上很多資料在介紹大頁內存的時候都會伴隨它在Oracle數據庫中的使用,這會讓人產生一種錯覺:大頁內存只能在Oracle數據庫中使用。通過上面的 分析,我們可以知道,其實大頁內存是一種很通用的優化技術。它的優化方法就是避免快表缺失。那么怎么具體應用呢,下面詳細介紹使用的步驟。
1. 安裝libhugetlbfs庫
libhugetlbfs庫實現了大頁內存的訪問。安裝可以通過apt-get或者yum命令完成,如果系統沒有該命令,還可以從官網下載。
2. 配置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個大頁可以被分配。
具體應該分配多少個大頁合適,這個需要多次嘗試,我們得到的一個經驗是:子線程對大頁的使用很浪費,最好是所有的空間都在主線程分配,然后再分配給各個子線程,這樣會顯著減少大頁浪費。
3. mount
執行mount,將大頁內存映像到一個空目錄。可以執行下述命令:
- 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。此時,程序申請內存就是大頁內存了。
按照上述四個步驟即可啟用大頁內存,所以啟用大頁還是很容易的。
6. 大頁內存的優化效果
如果你的應用程序亂序訪存很嚴重,那么大頁內存會帶來比較大的收益,正好我們現在做的聽歌識曲就是這樣的應用,所以優化效果很明顯,下面是曲庫為25w時,啟用大頁和不啟用大頁的程序性能。

可以看出,啟用大頁內存之后,程序的訪問時間顯著下降,性能提升接近50%,達到了性能要求。
7. 大頁內存的使用場景
任何優化手段都有它適用的范圍,大頁內存也不例外。前面我們一直強調,只有耗費的內存巨大、訪存隨機而且訪存是瓶頸的程序大頁內存才會帶來很明顯的 性能提升。在我們的聽歌識曲系統中,耗費的內存接近100G,而且內存訪問都是亂序訪問,所以才帶來明顯的性能提升。網上的例子一直在用Oracle數據 庫作為例子不是沒有道理的,這是因為Oracle數據庫耗費的內存也很巨大,而且數據庫的增刪查改也缺乏局部性。數據庫背后的增刪查改基本上是對B樹進行 操作,樹的操作一般缺少局部性。
什么樣的程序局部性較差呢?我個人認為采用哈希和樹策略實現的程序往往具有較差的訪存局部性,這時如果程序性能不好可以嘗試大頁內存。相反,單純的 數組遍歷或者圖的廣度遍歷等操作,具有很好的訪存局部性,采用大頁內存很難獲得性能提升。本人曾經嘗試在搜狗語音識別解碼器上啟用大頁內存,希望可以獲得 性能提升,但是效果令人失望,沒有提升反而導致性能降低。這是因為語音識別解碼器從本質上來講就是一個圖的廣搜,具有很好的訪存局部性,而且訪存不是性能 瓶頸,這時采用大頁內存可能會帶來其他開銷,導致性能下降。
8. 總結
本博客以聽歌識曲的例子詳細介紹了大頁內存的原理和使用方法。由於大數據的興盛,目前應用程序處理的數據量越來越大,而且數據的訪問越來越不規整, 這些條件給大頁內存的使用帶來了可能。所以,如果你的程序跑得慢,而且滿足大頁內存的使用條件,那就嘗試一下吧,反正很簡單又沒損失,萬一能帶來不錯的效 果呢。