ECS的簡單入門(一):概念

官方文檔:https://docs.unity3d.com/Packages/[email protected]/manual/ecs_core.html

視頻資料:UUG Online直播回放:DOTS從原理到應用-雨松MOMO

當前版本:Unity2019.3.0,需要在Package Manager中安裝Entities。

 

CPU與緩存(Cache)

首先我們先來了解一下CPU讀取數據時的操作,首先CPU會先從自己的緩存中去查找,如下圖,有L1/ L2/ L3三級緩存,若緩存中沒有找到需要的數據,則會去內存中查找(我們稱之爲Cache Miss),CPU讀取到內存數據後就會將新數據存放在緩存當中。CPU訪問內存的速度會比訪問L1 Cache的速度慢100倍,因此提高緩存命中率(Cache Hit),避免Cache Miss會大大提高性能。因此我們應該儘量使用數組,儘量分割屬性(SOA),儘量連續的進行處理。

這也使得一味的討論複雜度O(n)不再適用,因爲現在效率=數據+代碼,最常見的例子就是在數據量小的情況下遍歷數組會比 (Hash)Map 快上很多

緩存行

緩存又由若干個緩存行(cache line)組成,每個緩存行大概佔64字節。

假設我們現在想要旋轉並移動場景中的一個物體,那麼我們會修改它的Position(Vector3數據,3個float)和Rotating(Quaternion,4個float),其中1個float佔4字節,那麼一共佔28字節,那麼在這個緩存行就會有36個字節是完全浪費的,甚至我們可能只改了Position的x的值,這樣浪費的就更多。若有上千個這樣的方塊,那麼我們緩存中可能就會存在超50%的內存垃圾,從而導致緩存命中率大大的降低。

 

問題的引出

瞭解了上面的知識之後,我們再看回Unity,在傳統模式下,我們在場景中創建一個Cube,上面會有Transform,MeshRenderer,Collider等組件,而這些組件在內存中的排放都是無序的,這就會降低我們的緩存命中率。

除此以外,像我們前面說到的旋轉移動方塊,我們只用到了Position和Rotating兩個屬性,但是使用的時候整個Transform都會被加到緩存當中,而Transform中有很多我們不需要的屬性佔用了不少的緩存空間,同樣的降低了我們的緩存命中率。

使用ECS就可以解決上述的這些問題,從而提高性能。

 

ECS概念

ECS即Entity Component System,是Unity的一種框架,我們可以把Entity看做是一個唯一id,Component看做是數據,其本質是一個存放數據的Struct,一個Entity上可以擁有多個Component,但是Component中不會有任何邏輯處理。而System則可以根據Entities索引讀取對應的Component,對Component中的數據進行處理。

如上圖,有三個Entity,Entity A,Entity B,Entity C,與它們相關聯的Translation,Rotation,LocalToWorld和Renderer這些都是Component。同時還有一個System,用來將Translation和Rotation兩個Component內的值相乘並賦予LocalToWorld。

我們可以設置System需要Renderer Component,那麼Entity C將被該System忽略。或者設置System處理的Entity不能有Renderer Component,那麼Entity A和Entity B將被忽略。

 

Archetypes

不同的Component可以有多種不同的組合,每種組合我們即稱之爲Archetype。例如下圖,Entity A和Entity B的Component的組合是相同的,所以他們都屬於Archetype M,而Entity C由於少了Renderer,所以是另一種組合,屬於Archetype N。

由於可以在運行時給Entity動態的添加或刪除Component,若我們將Entity A的Renderder刪除,則此時Entity A會隸屬於Archetype N,若再刪除Rotation,則剩下的Component組合和Archetype M,Archetype N都不相同,就好隸屬於一個新的Archetype。

 

Memory Chunks

ECS會根據Archetype來進行分配內存,每個內存塊我們稱之爲Chunk,ECS會將符合Chunk對應Archetype的Entity放在該Chunk當中。一個Chunk中,內存地址是連續的,大小固定爲16KB。若Chunk裝滿了,則會生成一個新的Chunk用來存儲新生成的且Archetype符合的Entity。

由於我們動態的添加或刪除Entity的Component,會導致其Archetype變化,因此ECS也會改變其Chunk,放到與之對應Chunk中。

可能描述的不太好,我們來看示例圖。下圖中有三種Archetype,分別對應三種Component組合。每個Archetype都會有對應的Chunk用來存儲對應的Entity。若Chunk存儲滿了,就會在對應Archetype下新生成一個Chunk。

這樣的設計理念使Archetypes and Chunks是一對多的關係,同時若給定一個Component組合,我們要找到所有對應的Entity,只需要搜索現有的archetype即可,而不需要遍歷所有的Entity。

ECS不支持使用特殊的排序來將Entity存儲進Chunk中,若有一個Entity被創建或者被改變,使其隸屬於一個新的Archetype時,ECS會將其存儲在該Archetype下第一個還有空間的Chunk中。若有一個Entity被從Chunk中移除,ECS則會把該Chunk中最後一個Entity與其對應的Component移到這個空缺的位置中。

 

舉例

關於Entity,Component,Archetype和Chunk的關係,我們在此舉個簡單的例子。假設我們有兩個Component:C1和C2,然後我們生成五個Entity,其對應的Component分別爲:E1(C1,C2),E2(C1),E3(C1,C2),E4(C1,C2),E5(C2)。由於組合分別有C1,C2,C1C2三種,所以會有三個Archetype,假設我們一個Chunk只能存儲兩個Entity。那麼最終結果爲:

Archetype1:Chunk1 [ E1(C1,C2),E3(C1,C2) ]  -> Chunk2 [ E4(C1,C2) ]

Archetype2:Chunk1 [ E2(C1) ]

Archetype3:Chunk1 [ E5(C2) ]

 

結合到我們前面有關緩存的知識,若我們要移動選擇一個方塊,可能會寫兩個Component,MoveComponent存x和y兩個float(假設只前後左右移動),RotateComponent只存y一個float(假設只旋轉y軸),這樣我們的方塊就只會有3個float,佔用12個字節。若有多個相同的方塊,由於都有上述兩個Component,也就是屬於同一種Archetype,因此內存也是連續的。那麼在一個64字節的緩存行中,我們可以放下5個這樣的方塊數據,只有4字節是浪費的,就會大大降低Cache Miss。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章