標籤:

[CppCon16] Rainbow Six Siege - Quest for Performance

Rainbow Six Siege - Quest for Performance - Nicolas Fleury - CppCon 2016

Outline

Console上4個core共享L2

4個Build Options

Ubi使用RAD Telemetry

他們會檢測每幀的時間,異常的曲線即為卡點,具體使用circular buffer來維護一段時間的tag信息

而可以打開這一陣詳細的數據,來看看到底是什麼模塊耗時。Ps這個記錄的tag信息這麼詳細還不卡,有點厲害。。。

記錄了每個線程的tag,x軸是時間,y軸從上到下是記錄的callstack(主要看怎麼寫)

還可以加入string來記錄信息,不過是離線處理的(估計寫多了profile也吃不消,嚴重拖慢profile性能)

Tag的CPU開銷非常小,只有8B的數據開銷(多加幾個字元串試試),可以記錄CPU的時間片開銷,輔助檢測死鎖,記錄瑣碎的counters

舉了例子來說明

Tester工具可以根據changelist來跑兩個對比測試

跑一晚上,第二天對比一下

優點

2. 減少內存分配開銷

這是離ship還有幾個月前的profile,Waits一欄CPU在context switch,可以看到有大量的sleep,主要是在Allocator裡面有mutex等待

當時使用的是遺留的Allocators,嘗試使用了jemalloc,結果更糟(為啥不試試tcmalloc…)

優化

每個線程事先指定棧空間,每次做完job直接把棧頂置零,省去了delete和Destructor的開銷。Ps,而且額外的好處是,從邏輯上暗示了這塊內存不是線程間共享的,所有的操作都不需要考慮多線程的問題

只在初始化的時候設置一次當前的線程棧頂位置,判斷是否在棧內使用了當前的this指針和隨意的棧上變數比較地址,超過1MB(默認線程棧大小)的範圍即在棧外

*個人覺得可以做的更徹底點,data-oriented其中一個好處是可以在邏輯上避免大部分多線程bug,其意義就是可以給內存塊賦予state。最好把線程上所有的內存分配全部接管掉,一般job大部分內存都是自消耗,不需要和外部進行交互,直接看作單線程的代碼,它們可以全部使用棧上空間替代new出的堆上空間;除非需要產出結果或者同步的內容才會單獨allocate,而這些內容又需要特別考慮多線程之間的問題,所以最好由程序員自己在使用時就規定好內存的allocator的state。大量使用棧空間肯定會超過1MB,所以可以考慮使用類似有棧協程的方法,自己開闢半開的內存區間來使用,又避免了缺頁中斷一舉兩得。

3. 減少內存分配次數

Telemetry支持所有的內存分配開銷的callstack記錄,比我們少多了…

Compile time array,超過大小仍然會使用堆分配

目標是類似C#的信息

Windows上returnAddress可以追溯調用者

可以在Array構造的時候,保存所有的信息

在析構的時候,copy這些信息到一塊1G大小的內存。最終離線處理這些信息

最終處理的信息,每種類型的array,調用者,調用次數,大小等

這種分類排序有點意思。。。

這種級別的詳細數據基本可以coding期確定array的合適大小了,對管理內存簡直太友好了

Std::function的仿函數實現會捕捉一些參數,如果參數過多會使用堆進行內存分配,每幀大量的使用function會也造成潛在的大量內存allocation

提出inplace需求,不使用堆,預先分配好棧空間,size過大則static assert

分析lock,簡單的宏來保存mutex的name、callstack、加鎖的文件、加鎖的時間等

離線分析lock的工具

加上telemetry去分析長時間的lock

好像每個人都喜歡講這個…

代碼太多略過了

這些就不展開了

LockFreePool的8個node(8B)數據是一個typical

cacheline(64B),cache-friendly

競爭越激烈,越趨向於使用TLS,比如profile的數據全是tls

Lock-free實際並不代表free,還要考慮數據cache之間的同步。另外多說一句,雖然目前引擎也在大量使用多線程,但是並發量和伺服器端還是沒法比的,所以在低並發(相對的)的情況下,有時會Lock-free數據結構的性能其實並不是發揮的特別完美,甚至有時設計好的Lock的性能更好一些,所以一切都以Profile的實時說話。

用來檢測mem crash的allocator,在每個page的末尾分配給obj使用,並把下一頁page置位read-only。Free

obj之後,把當前page置為read-only。如果有對這段內存page的write,即mem corruption。

之後改進read-only為Uncommited,也禁止read。

*多線程尤其是使用了enqueue之後會造成很多隱藏的mem corruption,很多地方實在是太隱蔽了。之前我也想過用相似的辦法去做,但是覺得鎖page太浪費內存,只能利用重現去做硬體斷點來檢測,都是32bit exe的鍋啊…

哎,看來大家的解決bug的方式都是一樣,一把辛酸淚

Ubi的很多做法值得借鑒。和我們想的一樣,沒有一套完善的Perf工具是沒法去做好的Engine的,Ubi在此的投入顯然更多,工欲善其事必先利其器。

當Perf出現異常的峰值時,應該自動化的把perf數據dump出來,一直想做都沒做;完善的changelist build compare;統計分析所有allocation,基於數據改進簡化allocate,每幀的alloc是很費的,不單單用tcmalloc或者jemalloc簡單替換就完事的

這些做起來相比於engine都不是特別困難,像Fleury說的讓實習生都可以做好,但是其細節、離線分析、以及engine programmer怎麼用才是ubi高明的地方。


推薦閱讀:

[GDC16] Optimizing the Graphics Pipeline with Compute
[翻譯]Metal Gear Solid V – Graphics Study
[GDC17] Ghost Recon Wildlands:Terrain Tools and Technology
從零開始手敲次世代遊戲引擎(四十二)
[Siggraph15] GPU-Driven Rendering Pipelines

TAG:遊戲引擎 |