標籤:

[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

Memory Order

平台之間的差異

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:遊戲引擎 |