Lua性能優化—Lua內存優化

原文鏈接:Lua性能優化-Lua內存優化 - Blog,感謝作者舒航供稿,歡迎轉發分享,未經作者授權請勿轉載。當然,如果您有任何獨到的見解或者發現也歡迎聯繫我們,一起探討。(QQ群:465082844)

同時,作者也是U Sparkle活動參與者哦,UWA歡迎更多開發朋友加入 U Sparkle開發者計劃,這個舞台有你更精彩!

導語:大家好,我是舒航,現任職於心動網路,主要負責《仙境傳說RO:守護永恆的愛》(下簡稱"RO")的優化工作。今天主要想和大家分享一下,我這段時間在Lua性能優化方面的一些經驗。

現在市面上大部分Unity遊戲都能支持熱更,主要熱更Lua和一些資源。而Lua主要實現一些UI界面之類的非核心邏輯,這樣雖然Lua這部分代碼非常靈活,增加功能、修復BUG都非常方便。但是由於Lua實現的大部分都是UI,那麼Lua熱更出去的功能最多就是一些新UI界面罷了,或者用資源配合配置表來擴充下遊戲既有內容。如果想做個特殊的玩法勢必受到掣肘,一般最後都會發現導出的介面不全,非得打整包才行。為了使得RO的玩法更靈活,熱更的遊戲內容更豐富,其實就是為了滿足策劃們的各種需求啦,RO戰鬥邏輯的主體都是在Lua中完成。所以RO相對於其他的遊戲,對Lua代碼的性能要求會更高一些。

在我對Lua代碼進行性能優化的時候,主要分為兩部分:

1.內存優化

  • 常駐內存優化
  • 內存分配優化
  • 內存泄露優化

2.CPU優化

這篇文章主要是和大家分享第一點,在內存優化上的一些經驗和方法。在我進行Lua優化之前,我們已經請過UWA團隊進行過深度的優化了,而且收效很好,在此給UWA團隊贊一個。那麼,面對一個已經進行過一次徹底優化的項目,要進行更深入的優化之前首先要問自己一個問題:

Q: 我為什麼能比前人優化得更徹底,做到前人沒有做到得事情呢?

總的來說就是兩點:1)工具更先進,2)項目更熟悉。

所以在這樣的策略下,我主要是針對我們的項目編寫了兩個工具:LuaMemoryMonitor和LuaProfiler。一個用於優化內存泄露和內存分布,一個用於優化內存分配。有了更好的工具之後,再針對性地優化自己項目的代碼,無論是效率還是結果都非常好。LuaMemoryMonitor能在十幾分鐘內就定位到泄露的代碼,用這個工具我們一下午就查清了戰鬥中的內存泄露,並且能清晰地列出Lua的內存分布。而LuaProfiler幫助我們把Lua內存分配速度降低到了原來的40-50%。

LuaMemoryMonitor

LuaMemoryMonitor主要由兩部分組成: C庫Snapshot 和 UnityEditor ,下面我們分別說明。

一、C庫的實現細節

C庫的主要工作有:

  1. 快照_G
  2. 計算Lua對象內存大小
  3. 儲存快照

1.快照_G

Lua中可回收的對象遞歸關係如圖:

遍歷_G時按這個結構關係進行遍歷,只需要編寫5個函數traverse_object、traverse_table、traverse_function、traverse_userdata、traverse_thread,然後從traverse_table(_G)開始遞歸即可。在統計內存大小時,即要統計對象本身佔用的內存,也要遞歸地遍歷它所引用的所有對象佔用的內存。其中string是Lua內部管理的,統計時要注意同一字元串的不同引用不能重複統計內存。

2.計算Lua對象內存大小

對Lua當前內存進行快照時,我主要是快照_G,如果有需要的話可以快照Registry。如果僅僅是查內存泄露,那麼不統計_G內每一Entry的內存大小也可以。但是我為了分析內存分布,那麼必須得統計每一Entry的內存大小。統計Lua對象的內存大小是沒有現成的介面的,我這裡給大家提供一種思路,也是Lua源碼中統計對象內存大小的辦法。例如一個Table佔用的內存大小為:

3.儲存快照

由於要對Lua內存進行分析,那麼就不能把快照又存到Lua內,這樣很容易干擾自己的數據採集。這也是把快照的代碼寫在C里的一個重要原因,如果是在Lua里寫的話,就必須要小心翼翼地處理這些數據了。而寫在C里就很好解決了,我在C里用一顆多節點樹來儲存每一個快照,多個快照組成一個鏈表。

二、Unity Editor

1.Editor特性

Snapshot庫已經為編輯器準備好了所有數據了,現在只需要想一個好主意、好方法來利用這些數據。這裡我做了一個特別的功能,可以很高效地利用這些數據。在UnityEditor里可以對每個快照進行邏輯操作。

  • 求交集:即兩個或多個快照中都存在的對象,即這些快照中的常駐內存。
  • 求補集:A在B中的補集指在A中但不在B中的對象,即A相對於B的增量。

簡單例子:在剛進入戰鬥時採樣得到A快照,戰鬥一段時間後採樣得到B快照,離開戰鬥場景回到主城,手動GC後採樣得到C快照。求AB的補集得到一個新的快照D,D即是戰鬥期間新增的內存。求AC的補集得到快照E,E即是戰鬥期間新增並且離開戰鬥GC後沒有釋放掉的內存 。對D和E求交集,得到快照F,F即是戰鬥中新增但是回到主城後釋放掉的內存。這其中E中的對象非常有可能就是泄露了的內存,而F中的對象是可以嘗試更早地釋放的內存。這時可以選中E快照,把E快照輸出到Editor上,輸出為一個樹狀結構,就像Unity自帶的Profiler中一樣,如果有泄露的話基本上就無所遁形了。而直接輸出A快照的話,就能得到剛進入戰鬥時的內存分布。

三、常見泄漏:C#代理

我們項目中查到的比較多的泄露就是C#代理了,如果把Lua匿名函數註冊給C#的代理,那麼這個Lua匿名函數將不能正確地被LuaGC了,也就是泄露了。改進方法就是不把Lua匿名函數註冊給C#代理,這樣的話,每隔一段時間C#都會主動Dispose。

四、其他內存優化:常駐內存優化

常駐內存方面我們一直控制的很好,這次我們主要是優化了大量的table配置表,這個優化主要參考了UWA中的這篇文章【 Lua配置表存儲優化方案—盧建】

五、LuaMemoryMonitor改進

現在LuaMemoryMonitor定位是一個快速定位泄露的工具,還需要提前知道疑似泄露的地方,然後有針對地採樣。有一個改進方法是,在Lua中寫一個定期檢查內存疑似泄露的工具,然後在疑似泄露的地方用LuaMemoryMonitor進行快速定位。

LuaProfiler

解決了常駐內存和內存泄露的麻煩後,發現內存增長得很快,很短時間內就會到達我們項目設置的閾值,迎來一次GC。而剛GC完空餘出來的內存又會迅速地被分配出去,內存長時間處於高位。頻繁分配內存不僅降低了性能,還使手機更容易發熱了。為了定位和優化內存的高分配,我模仿Unity的Profiler寫了一個LuaProfiler,並做了相應的擴展。這個工具也由兩部分組成:1)C庫 , 2)UnityEditor。

一、 LuaProfiler的C庫

LuaProfiler的C庫主要完成了數據採集的工作。它在Lua虛擬機中註冊了鉤子函數,每次Lua Call 和 Return 的時候都會觸發回調。在每次回調的時候,在C里維護了一顆限制層級的多節點樹,由C#主動來取這顆樹。每幀都取每幀都清空,即是逐幀統計。每隔一段時間取,但不清空,即是累計模式。

二、 LuaProfiler的UnityEditor

Editor的主體是一組遞歸繪製的foldout控制項,在Editor上顯示出一個樹狀結構。LuaProfiler能實時地根據調用層級,然後通過各個按鈕實現各種規則排序。這和UnityProfiler相同,想必大家不會陌生。另外LuaProfiler不僅提供了和UnityProfiler一樣的逐幀模式,還提供了統計模式和累計模式。在累計模式下,會把每一幀的內存分配累加起來,以樹狀結構的方式展示出來。而統計模式則是把每一個函數的內存分配累加起來,以Top10的形式展現出來。統計模式不關心調用層級,只關心所有函數中哪些函數分配的內存最多。在這樣一個工具的幫助下,RO的內存分配優化變得格外簡單、高效。

三、 常見優化:string.gsub和string.gmatch

在我們項目中,用這兩個string庫函數完成了一個計算中文個數、長度的工具函數。但是容易被忽略的是,string.gsub 和 string.gmatch 會產生大量的子串,這些子串都會開闢一片內存,而我們根本用不上這些子串。我發現函數1在很多項目中都普遍存在,但是用函數2會更好一些。

四、 常見優化:Lua中String是不可變值

這一點也經常被大家忘記,哪怕是寫Lua的老手。在以下代碼中,因為Lua的string是不可變值,每次拼接都會產生一串新的字元串。第6行會產生"仙境"、"仙境傳"、"仙境傳說"一共3串字元串,但是我們只是需要第三串而已(「仙」字被Lua背部重用了)。這無形中就多開闢了一部分內存,我們可以對以下代碼進行優化,從而避免浪費。這種疏忽經常出現在 I/O文件、聊天頻道、處理配置等描述欄位時發生。

五、 常見優化:內存池

如果想降低內存分配速度,使用內存池復用對象是必不可少的。在Lua內存池的使用過程中,最容易出現的問題是,忘了放回池子以及池子大小不合理。

總結

全文上下,用到的技術都不難,相信大家都能搞定。在這裡主要是和大家分享一下拿到原始數據後,如何處理過濾數據、信息的經驗,從而更快更準確地定位問題。如果大家有更好更精準的處理數據、過濾信息的方法請不吝賜教。

筆者聯繫方式

郵箱:inkiu0@gmail.com

QQ: 286553528

UWA群內ID:ink_U0

文末,再次感謝舒航的分享,後續他將會給大家帶來LuaProfiler的CPU優化部分,主要內容包括:

1)用多線程緩解DeepProfile數造成的卡頓 ;

2)Lua的CPU時間優化經驗;

3)Lua的CPU時間到底消耗在哪一塊,Lua是不是不堪大任呢?

歡迎大家的關注,也歡迎大家來積极參与U Sparkle開發者計劃,簡稱"US",代表你和我,代表UWA和開發者在一起!


推薦閱讀:

Lua程序逆向之Luac文件格式分析
用LuaStudio調試Unity中SLua里的Lua5.3代碼
維基百科中模板和模塊有什麼區別?
公式計算機的另一種實現思路

TAG:Unity游戏引擎 | Lua | 性能优化 |