Fiber based job system

N 人看过

Preface

工作上使用unity開發遊戲時,在效能有瓶頸的區塊時常會使用到Job System來提升效能,
去年因為興趣使然稍微研究了Job System的底層實作,在公司內部報告使用過,
藉著最近有點時間將這些資訊整理起來發成部落格文章。

寫完之後回頭來看,必須先提醒一下,這篇會使用到相當多作業系統的背景知識,
建議讀者需要先具備作業系統的基礎再做閱讀。

What is Job System

Job system是一種管理多執行緒(multi-thread)程式的封裝,藉由將執行的程式碼打包成一個個任務,來抽象化執行續本身的使用。

Job System藉由管理一組 worker thread進行跨CPU core的資源使用,通常使用與CPU core數量相同的worker thread數量,來避免頻繁的上下文交換(context switching)所帶來的效能損失。(即使作業系統或其他程序可能會使用到一些核心的資源)

Job System 藉由將 Job 推入一個 Queue (或是priority queue),將他們依序放入worker thread執行,同時Job System會管理他們之間的依賴關係來確保 Job 依照邏輯順序進行。(像是NPC走到定點後,才開始進行巡邏)

Naive Job System Structure

  • Worker threads : 使用std::thread::hardware_concurrency() 獲得CPU核心數,生成對應數量worker thread
  • Job queue : 由各個worker thread在空閒時自己抓取,所以需要是thread safe的資料結構
  • Counter : 紀錄目前還在進行的job數量,可以使用 std::atomic

Fiber

About Fiber-based Job System

Naughty Dog在GDC 2015的演講是提出這個實作並且推進遊戲使用的非常重要的一篇文章,非常推薦大家閱讀。

What is Fiber?

根據 Microsoft Win32 的 Manaul 所述:

A fiber is a unit of execution that must be manually scheduled by the application. 

Fibers run in the context of the threads that schedule them.

Fiber具有以下特性:

  • 一個小型的 thread,由使用者提供stack space,並且具有更小的 context,可以將register暫存在stack上
  • 由 worker thread 執行
  • Cooperative (合作性) 的多執行緒執行,由fiber之間yield做切換,而不主動搶佔
  • 極小成本,不會有thread的context switching在fiber切換時發生 (只有register的save/load必然發生),因為context都還在stack上

同上所述,Fiber 之間的切換是使用yield進行,也就是所謂的cooperative scheduling,Fiber將這個cpu scheudling和context switching(kernel-space)的操作拉到使用者(user-space)進行,並且讓這些操作成為寫程式的基本步驟。
這使得工程師可以在多執行緒的程式設計中取得更多的操縱權。

在各式各樣的語言中都有著類似的東西,更多時候被稱為User-space thread或是green thread(Ruby, Java)

有些語言也有著coroutine的概念,fiber和coroutine也有相當程度的相似,只不過coroutine通常是語言層級的概念,而fiber通常做為系統層(system-level)的概念存在。

Fiber 的實作

Fiber的實作是在組合語言層級進行,將所需的register存放到對應的stack空間,再藉由jmp跳到其他的上下文執行。
如果沒有瘋到想自己實作,大可使用OS Support的Library來使用
例如:Win32Unixboost/context

FTL (fiber-based tasking library)

為了進行實作練習,我找到一個滿完整的fiber job system開源專案進行參考

RichieSams/FiberTaskingLib

FTL使用上面提到的boost作為fiber的實作

FTL 的 Fiber 實作

Yield to other task

分成兩個區塊進行,如圖:

第一部分是使用boost/context的api,將context打包成fiber bundle,儲存到stack上,並且將目前的task丟進等待 queue 中

第二部分則是從queue中找尋目前等候執行的task,將他填入目前的TLS (thread local storage)

最後使用fiber api進行user-space的context switch就完成切換

如何使用FTL

  • Caller
    呼叫Job執行的部分,需要執行底下步驟
  1. 初始化 job scheduler (in stack)
  2. 創造task陣列 (也是stack)
  3. 初始化job的參數
  4. 將job kick到scheduler中
  5. 等待atomic counter歸零 (job執行完成)

程式碼如圖:

  • Callee
    作為Job的function必須符合底下格式:

如圖所示,送入目標的task scehduler以及參數來執行job化的程式碼

簡單的一個例子如下:

這是簡單對數字進行加法的程式,

第一塊是主要邏輯進行的部分,也就是我們要平行化的部分

第二塊是假如後面程式碼需要依賴其他task的結果,可以將目前context暫存,把執行資源yield給其他task執行,
等待依賴的task執行完畢

第三塊則是可以從這個job中開始其他的job的執行。

實作

我使用了FTL進行經典的flocking演算法boid的實作,
2000隻的鳥群,在單核心的執行如下:

使用job system優化後如下:

還有進行一些kick now, gather later,以及double buffering之類的技巧優化
以及將render thread和game logic thread拆開,因為不是這篇主題的重點,就不細講,
有興趣的可以參考實作的Repo:

Harrison-Dev/Boid-Jobify

別忘了幫我留下一顆星星!

Reference

Job concept
Fiber Concept
MS Fiber
Naughty Dog’s Slide
Fiber Tasking lib
Fiber vs Coroutine