[CppCon14] How Ubisoft Montreal develops games formulticore – before and after C++11
How Ubisoft Montreal Develops Games for Multicore - Before and After C++11 - Jeff Preshing - CppCon 2014
05年後多核CPU逐漸成為主流
Part 1
單線程模型
Ubi三種線程模型
- Pipeline work
2個thread互相之間lock step的執行
- Dedicated thread
1個線程連續的執行任務
- Task schedulers
目前的大部分Engine做法
Pipeline work
(主)線程來處理大部分邏輯,另外一個線程來處理圖形。需要一些同步機制(信號量)來同步兩個線程,告訴渲染線程何時開始(主線程邏輯準備好之後),告訴主線程邏輯不要運行到N+2幀(N的渲染loop結束後才能開始)
那麼N~N+1的跨度就帶來一個問題,對於每個GameObject,如何避免在渲染的時候慘遭主線程毒手的修改。
1. Double buffer
每個GO兩份,分給N和N+1幀
2. GraphicObject
每個GO單獨抽象出一個Graphic的Ojbect,對於渲染線程Consume ReadOnly
Dedicated threads
對於載入遊戲內容,有一個Loading線程
大部分情況,loading線程無事可做在sleep。如果player從一個區域move到另外一個區域,那麼主線程會發給loading線程一個請求,載入新區域的內容。Loading線程一般是IO bound而不是computing bound。Loading線程不需要和主線程進行lockstep的同步,所以大部分情況下使用queue把任務扔進loading的隊列即可。
*多說兩句,記得之前看過一些資料,說IO
bound的loading一般不要開太多的線程去做,因為並不是開多了threads就可以load更快,反而在尋道磁碟類型的硬碟上增加開銷時間。其中一個做法是開一個專門io的進程來集中處理io業務,進程的好處是在多開客戶端時,可以公用IO上的loading request,當然通信和編碼也更麻煩一些。*另外,loading線程也不是全然沒有同步的,資源載入完成後還需要回調進行處理,這些邏輯大部分需要在主線程進行,所以每幀的開始或者結束需要主線程輪詢或者用信號量等進行通知。有一些需要實時載入的邏輯,比如音效、碰撞檢測,如果不載入資源進來就無法繼續進行遊戲。
Improve on the queue
1. cancel
2. interrupt
3. re-prioritize
Task scheduler
細化任務(jobification)
把jobs放在每個worker thread去做
把job分組
相當於從job池裡去取任務來執行
每個worker thread所處理的task
groups可能會有所不同管理任務依賴
做完最後一個job的線程去issue下一個taskGroup
可以做很多自定義的優化
*多說兩句,個人還是覺得Ubi的這個taskScheduler不如Naughty Dog的,個人認為worker線程+主線程的job邏輯有缺點:主線程負責調度,避免不了自身的sleep,worker線程也一樣,worker的while循環相當於不停的spin,為了避免浪費cpu計算資源肯定會設計wait的邏輯來讓渡。這樣會造成大量線程的context switch,並且這個cs是kernel層面的,thread state從sleep->ready->run並且會有大量的等待時間來等待os分配cpu time slice,無形中又增加了時間。另外從cache的角度考慮,每一次cs都會再次需要預熱cache,造成大量cache
miss,fine-grained的jobs更增加了這個成本。Naughty Dog的解決方案思想是沒有主線程的概念,所有的線程都是affinity到每個物理core上的,不進行kernel層面的線程調度,讓所有線程一直處於run state,這樣的話需要進入fiber的yeild進行類似callback的回調,利用這種方法來manage task,類似拓撲排序的方式來進行所有的任務,這個等以後有空在另開一篇吧Atomic Operations
可見上世代的atmoic…
Fence
平台之間的差異
MemoryOrder的bug,在XBOX這種weakly ordered機器上,上圖兩種reorder的方式都會造成read的item還沒有被push。
解決方法加入order fence
這個還是Herb講的好一些,下次在寫
搞笑的Q&A來了,有個人問什麼情況下會使用memory order sequentially-consistent,結果人家說目前在Ubi mtrl沒有一個人知道這是啥。。。
Part 2
C++11 Atomic Library
嗯,實話
Keep it simple
主要是CPU single instruction r/w數據大小造成的問題
Legacy code…話說volatile保證不了原子性吧
Java volatile…
排列組合
因為沒有memory order,被reorder了
default arg
排列組合again
Ubi14年還沒用Atomic Lib
對於low-level的模型,可以想像成每個線程都有自己的一份數據拷貝,跟每個core都有自己的L1 cache一樣,只不過同步cache需要時間
Ubi有自己的migration對應
Low-level atomics example
這兒我不是很懂,fence不是compiler層面的嗎,只會保證在編譯器沒有reorder,怎麼會在運行期sync,是什麼特殊的CPU instruction嗎,求大神指點…
相同的寫法
SC到底做了什麼會這麼慢,求大神賜教。。。以我的理解,aquire/release語義首先在compiler層面保證不被reorder,其次可能會強迫刷新同步cache,這兩項可能會帶來流水線的penalty和latency。但是sc語義不清楚,具體實現如何為啥這麼慢。。。
推薦閱讀:
※[Siggraph15] GPU-Driven Rendering Pipelines
※從零開始手敲次世代遊戲引擎(四十二)
※[GDC16] Optimizing the Graphics Pipeline with Compute
※Matrix and Transform Conversion 1/3
TAG:遊戲引擎 |