標籤: Parallel

Unity 平行化探討 – C# Job System

前言

以現在遊戲用的電腦而言,有8個thread以上可以平行處理的CPU已經相當普遍,但是如果遊戲程式沒有使用到平行化技術,還是只能使用一個thread來執行,造成效能瓶頸。

在Unity當中,開發者所寫的程式如果沒有自行開啟新thread (在最新版的Unity中以可使用C#開新thread),那麼主程式都是由一個main thread在執行,雖然Unity內部會開一些thread來做渲染之類的工作,增加效能,但是開發者自定義的部分不會被平行到。

自己使用C#的thread會遇到很多問題,很多重要數值,尤其是與物理引擎有關的部分,像是transform的position, 剛體的velocity等等,都是只能在main thread寫入的,因此我們需要使用Unity提供的一些平行化方式。

順道一提,Unity的協程(Coroutine)並沒有平行化的效果,依然是在main thread上執行。

概觀

C# Job System可以在Unity 2018.1 beta中使用,主要概念是將相同Component中Update()裡面的內容提取出來控制,光從這點上就能減少因大量Component產生的大量Update()呼叫帶來的效能損耗。

例如本來是很多Component在Update時,都讓transform根據速度移動一點。現在將移動的這段程式移到一個獨立的Component,原本的那些GameObject變成用一個陣列的形式來存取,每次這個獨立的Component做Update的時候,將陣列裡的GameObject掃過一遍,讓每個都依照他的速度動一點。

Job System中,會定義一些Job,這些Job會需要預先定義會使用到那些資料,並定義要怎麼使用這些資料。

例如

struct PositionUpdateJob : IJobParallelForTransform
{
    [ReadOnly]
    public NativeArray<Vector3> velocity; // the velocities from AccelerationJob
    public float deltaTime;

    public void Execute(int i, TransformAccess transform)
    {
        transform.position += velocity[i] * deltaTime;
    }
}

這是一個定義position更新的job,所有的job都會是struct,並根據需要繼承不同的interface,在這裡我們會需要更新transform的值,所以繼承IJobParallelForTransform。

在這job被建立的時候,我們需要給定一個velocity的array,而在job執行時,我們要給定一個受影響的Transform array,這個array當中的每個transform都會被傳遞到Execute方法中執行。

i表示現在執行array中的哪個index,TransformAccess transform表示現在存取到的那個transform。

我們透過一個官方範例來理解怎麼使用Job System。

範例

完整官方範例請參考 https://github.com/stella3d/job-system-cookbook

首先打開 https://github.com/stella3d/job-system-cookbook/blob/master/Assets/Scripts/AccelerationParallelFor.cs

使用Job System的時候要using Unity.Jobs與UnityEngine.Jobs (第3,4行)

BaseJobObjectExample是一個簡單的MonoBehavior類別,他提供快速建立一大堆物件與提供存取物件資料的陣列,你也可以根據自己的需要製作,不必使用他的類別。

看到Start(),首先先建立很多個遊戲物件,範例中預設建立10000個物件,並在這裡預先建立好需要使用的資料陣列。

接下來是定義兩個job,第一個PositionUpdateJob 是剛剛看到的,根據給定的速度來更新位置的job。第二個是AccelerationJob,用來計算速度的job。

我們需要先計算每個物件的速度,然後再根據這個速度更新每個物件。

Update()中,每次我們就新建這兩個job,並給定參考的陣列。

透過m_AccelJobHandle = m_AccelJob.Schedule(m_ObjectCount, 64);

規劃m_AccelJob,總共有m_ObjectCount個物件要計算速度,內部一個批次計算64個。

再來執行m_PositionJobHandle = m_Job.Schedule(m_TransformsAccessArray, m_AccelJobHandle);

這是讓更新的位置依賴於計算速度的位置,每個物件要計算位置時,速度要先算完。

在這邊建立好相依性後,job不會馬上完成,只是進行規劃。直到LateUpdate()中,m_PositionJobHandle.Complete(),這裡會等到job中的所有運算都完成後才會繼續執行下去。

 

整個Job System的觀念是定義好job所需的資料與job之間的相依性,然後同步執行job的內容。因為i是獨立的,所以可以分給很多個worker去運算。Job System會建立所有thread數-1個worker來執行job,可以在profiler中觀察到,像是有8個實體thread的話,除了main thread,會另外有7個worker來算job,使用的好的話可以大幅提升遊戲效能。