2010年8月27日 星期五

NUC501-虛擬記憶體問題的最終回

上回出現RAM虛擬記憶體效能差勁,卻不知原因為何的謎團後,經過詳細的(?好啦!我承認是亂槍打鳥的)檢查、抽絲剝繭,測試各相關軟體元件的執行效能,造成效能瓶頸的犯人終於找到了!原來犯人就是「帕雷拖法則」!

根據維基百科記載,帕雷拖法則是:

帕雷托法則(Pareto principle),也稱為80/20法則,此法則指在眾多現象中,80%的結果取決於20%的原因...在電腦科學裡,帕雷托法則可藉由觀察80%的資源是由20%所操作使用,來最佳化資源。在軟體工程上,常有接近90%的電腦程式執行次數花費在10%的程式原始碼執行。

而在這個專案中,效能低落的發生與程式撰寫方式有很大的關係。由於專案開發的時候,常喜歡將使用到的功能,寫成群組型態的功能區塊。例如有專門處理LCD顯示的gfx系統、處理文字顯示的font系統,以及處理檔案系統的APSLib等等。而各功能區塊又會進行細分,有時會製造出許多單一功能的小函式。

之後為了避免上層程式執行功能時,都得要呼一堆小函式,所以又建立easy系列的函式,比方說EasyOpenFile之類的,這樣想要很輕鬆愉快的寫上層程式,就用easy系列,想要easy系列之外的低階控制就另外自己呼小函式。當同樣的低階控制程式碼多了起來,就又會為它增加了一個easy函式。上層程式寫好以後,它的上上層程式大多會形成任務分派形式的程式碼(dispatcher),因此完成版的程式碼很有大腸包小腸的Fu。大金每次看我的程式,都說我的主程式看起來簡單到不行(因為只是任務分派的迴圈而已),可是副程式卻呼來呼去複雜到死。

不過這種寫法是以前教科書上的建議(相信有很多程式設計師也是這麼做),這麼多年下來執行也都沒什麼大問題呀!為何這次會踢到鐵板呢?

主因是:這種程式寫法不適用在目前所設計的虛擬記憶體架構!

比方說,執行主程式時,會因為主程式本身是任務分派的形式,又因底下程式細分的關係,造成虛擬記憶體得從各個不同的區塊載入這些細分的程式,程式層數愈深,頁面切換的次數愈多,最後頁面也許就全部被這些程式碼佔據。當返回主程式時,主程式早已從頁面中消失,這時虛擬記憶體必須將主程式切換回來,如果主程式是迴圈構成的,又得繼續進行上述的處理。

發現問題的所在了嗎?問題就在於主程式(及各功能的頂層程式)本身所需的CPU時間幾乎不到1%,卻要勞師動眾將它從SpiROM複製到虛擬記憶體頁面上,然後才執行不到幾個指令,又被置換出去!所以當程式像洋蔥一樣,一層包一層,層級愈高的程式碼愈會形成任務分派形式的情況下,CPU的效能就被這些執行時間不到1%的程式碼給消耗掉了!那該怎麼辦呢?

一個方法是打掉重寫,用inline的方式執行函式,這樣會使得虛擬記憶體頁面切換有點像是循序進行,減少效能大起大落的問題,但程式碼編譯出來會非常巨大,這對大金想用小容量的SpiROM是完全不可行。

另外一個方法,則是多年前看的文章所引發的靈感。文章內容是探討PowerPC晶片的cache lock指令,對於多媒體處理的影響。由於多媒體資料大多為一次性且大量,如果沒有執行cache lock,將多媒體處理用的程式碼鎖定在快取內,很快的,CPU的快取便會充斥這些一次性的多媒體資料,而造成多媒體處理的效能低落!除此之外,Apple的麥金塔電腦,從68K系列的CPU轉換到PowerPC時,也是透過cache lock的方法來提昇68K模擬器的執行效率。之前研究NDS時,許多遊戲程式要播放即時影片前,都會透過NDS CPU的cache lock功能把處理程式鎖在快取內,以提昇播放的順暢度。這讓我想到,應該把這個虛擬記憶體系統當成快取系統來看待,才能在寶貴的RAM中放需要提昇速度的程式碼,而不是那些任務分派用的程式!

原先想在虛擬記憶體系統放個旗標,代表lock,這樣虛擬記憶體切換頁面時,就忽略這個頁面,頁面的鎖定就由想要留在頁面的程式碼主動進行lock好了。但一想到必須在各個系統放CacheLock及CacheUnlock之類的函式就有點暈了,再加上大腸包小腸形式的程式碼,還得要小心不能把所有頁面都lock(其它程式就不能執行了),其複雜度光想就腦袋打結了。

在反覆思考實作的方法卡了好久,只好試著反向思考,以結果來說,我們希望這些執行時間1%以下的程式碼,不要進入虛擬記憶體頁面執行,那NUC501有什麼硬體能達成呢?突然想到答案就是之前虛擬記憶體亟欲取代的SpiROM!雖然SpiROM速度不快,但能夠執行程式呀,跑這些任務分派的程式碼應該夠了!

趕忙修改ld的設定檔,增加一個NONCACHE的程式節區,而區段就在SpiROM所在的0x40000000位址上,緊鄰可被載入虛擬記憶體的程式碼之後(因為這些程式碼本身仍需要SpiROM當載體),這樣連虛擬記憶體系統都不用增加旗標什麼的,直接就能自由呼叫分散在虛擬記憶體與SpiROM上的副函式!

接下來的工作就比較麻煩,必須挑出哪個函式是屬於任務分派類型的程式碼,但排除performance senstive的函式,挑出來之後,便為它們標上NONCACHE巨集,這樣在編譯時,這些函式會自動被安排到NONCACHE節區中,屬於最大的任務分派函式main()也不例外!在編譯完成後,我緊張的把程式碼燒到開發版上測試。

結果效能提昇的數字令人驚訝,原先APSLib令人難堪的12K低速,效能一整個提昇26倍,得到312KB的讀取速度,字型顯示的處理時間也低於1秒了,雖然仍比原生讀取速度低,但已經足夠,只要專案完成時效能不是太低,我想應該不會再改進這個部份了。

那麼有關探討NUC501上虛擬記憶體實作的問題,應該就在這篇結束了。如果有在看這個網誌的朋友,也歡迎提出您的見解與問題,一起討論哦!

沒有留言:

張貼留言