背景介紹
性能測試是 SDK 發(fā)版的重要依據(jù),VolcRTC 的業(yè)務(wù)方對于性能指標(biāo)都比較重視,對于 RTC 準(zhǔn)入有明確的準(zhǔn)入標(biāo)準(zhǔn)。因此我們建立了線下的性能自動化測試系統(tǒng),測試過程中我們發(fā)現(xiàn) VolcRTC 的內(nèi)存占用較高存在較大的優(yōu)化空間。某個版本 1v1 語音通話 VolcRTC 1v1 語音通話內(nèi)存占用:
占用的資源 | Memory[MB] |
Android 高端機 | 17.87 |
Android 中端機 | 17.58 |
Android 低端機 | 16.06 |
iOS 高端機 | 6.19 |
iOS 中端機 | 6.52 |
iOS 低端機 | 5.73 |
為了實現(xiàn)內(nèi)存優(yōu)化,首先需要理清兩個問題:
- 哪些模塊消耗多少內(nèi)存?
- 如何優(yōu)化?
內(nèi)存組成
在回答以上兩個問題之前,我們先了解下內(nèi)存的主要組成部分有哪些。
在 Android 系統(tǒng)上,內(nèi)存主要分為:
下圖紅框部分為 VolcRTC 通話過程
- Java Heap,從 Java 代碼分配的對象;通話過程中 Java 內(nèi)存的分布曲線,主要呈鋸齒狀的周期性變化。結(jié)合 VolcRTC 的業(yè)務(wù)特點,可以知道這部分內(nèi)存主要在 JNI 調(diào)用時分配臨時對象,累計到一定程度后由系統(tǒng)的 GC 機制回收。
- Native Heap,從 C 或 C++ 代碼分配的對象。這部分為 VolcRTC 主要內(nèi)存占用。
- Code,用于處理代碼和資源(如 dex 字節(jié)碼、經(jīng)過優(yōu)化或編譯的 dex 代碼、.so 庫和字體)的內(nèi)存。VolcRTC 庫所占用內(nèi)存,但不等于動態(tài)庫的包大小,主要原因在于代碼段是按需分頁加載的,所以部分代碼不會被加載到內(nèi)存。VolcRTC 是一個動態(tài)庫,因此 Code 的內(nèi)存也是在通話過程中主要部分。
優(yōu)化方向
根據(jù)上文的初步分析,可以確定 VolcRTC 的內(nèi)存占用主要分布在 Native Heap 與 Code 段。因此我們明確大體的優(yōu)化方向為:
- Native 內(nèi)存優(yōu)化
- 動態(tài)庫包體優(yōu)化
內(nèi)存歸因分析
哪些模塊如何消耗多少內(nèi)存?
- 內(nèi)存分配堆棧信息
- 按模塊歸因
Heapprofd 實現(xiàn)原理
- hook malloc、calloc、realloc、free 等內(nèi)存分配相關(guān)的函數(shù)
- 拷貝寄存器與棧內(nèi)存,存儲到共享內(nèi)存,用于?;厮?/li>
- 根據(jù)堆棧信息聚類生成 Trace 文件
模塊歸因
VolcRTC 歸因規(guī)則
VolcRTC 主要分為底層媒體引擎與上層 RTC SDK 兩部分。媒體引擎的整體架構(gòu)是以流水線(Pipeline)的形式組成的,每個 Pipeline 由實現(xiàn)不同功能的 Node 構(gòu)成。我們可以根據(jù)相關(guān)的命名空間進行堆棧過濾,再根據(jù)軟件分層架構(gòu)進行層層歸因。
純系統(tǒng)堆棧
VolcRTC 引起的系統(tǒng)堆棧內(nèi)存分配,堆棧不包含 VolcRTC 符號信息,無法按前述規(guī)則歸類,需要歸類到由 VolcRTC 引起的系統(tǒng)內(nèi)存分配。
歸因示例
內(nèi)存分配堆棧特征一般為棧底為??__pthread_start(void*)?
?,棧頂為內(nèi)存存分配方法,中間為 VolcRTC 堆棧信息。根據(jù)堆棧信息,結(jié)合歸因規(guī)則然后層層向上歸因,形成一個樹狀的結(jié)構(gòu),準(zhǔn)確分析每一個 Pipeline、每一個 Node、每一個類型的對象所占用的內(nèi)存大小。
碰到的問題
Hook malloc 得到的內(nèi)存大小與 Native Heap 大小不一致
malloc 向內(nèi)存分配器申請的內(nèi)存,跟程序運行時傳入的 size 一致。
內(nèi)存分配器向操作系統(tǒng)申請的內(nèi)存按頁分配,一般每頁為 4K,Native Heap 統(tǒng)計的是這部分的內(nèi)存大小。
由于內(nèi)存分配器的需要反復(fù)分配與釋放內(nèi)存,不可避免的產(chǎn)生內(nèi)存空隙也就是內(nèi)存碎片,另外內(nèi)存分配器會緩存一部分小內(nèi)存塊以提升內(nèi)存分配效率。
語音通話內(nèi)存分析
通過性能自動化測試工具,生成分析報告。基于分析報告我們繪制語音通話內(nèi)存全景圖,再通過全景圖識別出內(nèi)存占用較高的幾個模塊,指引優(yōu)化方向。
內(nèi)存優(yōu)化
編譯優(yōu)化
包大小會直接影響到內(nèi)存大小,因此優(yōu)化包大小也可以有效減少內(nèi)存大小。通過打開 LTO、Oz 等編譯選項,結(jié)合線下性能自動化測試評估是否對性能指標(biāo)有負面影響來決定需要開啟的編譯優(yōu)化選項。編譯優(yōu)化后 Android 端動態(tài)庫包體減少了 900KB,通話過程內(nèi)存優(yōu)化 850KB 左右。
按需動態(tài)分配
VolcRTC 作為一個通用功能的 SDK,對于每個特定場景會有很多冗余邏輯與功能,這些邏輯與功能都存在較多的預(yù)分配內(nèi)存。對應(yīng)的優(yōu)化方案是:
- 合理代碼組件化,將不同的功能抽象成組件做到靈活組裝與按需加載,如:AI 降噪功能內(nèi)置的數(shù)據(jù)和模型會占用較大的內(nèi)存空間。
- 內(nèi)存盡量按需動態(tài)分配。如:AEC 回聲消除在不同場景下有不同的算法,需要根據(jù)實際的場景按需分配內(nèi)存,減少過多的內(nèi)存預(yù)分配。
設(shè)置合理的緩存大小
不合理的緩存大小也會引起不必要的內(nèi)存浪費。通過內(nèi)存的歸因分析,結(jié)合不同場景的業(yè)務(wù)特性,設(shè)置更加合理的緩存大小,可以減少內(nèi)存占用。例如:RTC 采用了 RTP 包重傳機制來對抗網(wǎng)絡(luò)丟包,為了實現(xiàn)重傳機制,需要緩存一定數(shù)量的包,緩存的數(shù)量需要跟進幀長、實時性要求等業(yè)務(wù)特性來設(shè)置合理的值。
合理的算法和數(shù)據(jù)結(jié)構(gòu)設(shè)計
合理的算法和數(shù)據(jù)結(jié)構(gòu)也可以有效降低內(nèi)存。在保證計算準(zhǔn)確性的前提下,通過減少數(shù)據(jù)值域范圍,使用內(nèi)存空間占用更小的數(shù)據(jù)類型來實現(xiàn)算法,比如統(tǒng)計與時間相關(guān)的數(shù)據(jù)時使用相對時間而非絕對時間、空間音頻算法通過定點化使用??short?
?類型代替浮點型數(shù)據(jù)。另外數(shù)據(jù)結(jié)構(gòu)設(shè)計時需要考慮內(nèi)存對齊問題。
優(yōu)化效果
1v1 語音通話
占用的資源 | 優(yōu)化前 Memory[MB] | 優(yōu)化后 Memory[MB] |
Android 高端機 | 17.87 | 13.59 |
Android 中端機 | 17.58 | 13.98 |
Android 低端機 | 16.06 | 12.93 |
iOS 高端機 | 6.19 | 3.87 |
iOS 中端機 | 6.52 | 3.84 |
iOS 低端機 | 5.73 | 3.14 |
本次內(nèi)存優(yōu)化,我們探索了 RTC 場景下性能歸因分析驅(qū)動性能優(yōu)化的實踐。可以總結(jié)出以下經(jīng)驗:
- 構(gòu)造穩(wěn)定的測試用例
- 建立性能折損的數(shù)據(jù)歸因模型
- 基于歸因模型識別熱點性能問題,形成優(yōu)化方案
- 從 1v1 通話開始分析,然后逐步到多人、百人千人。