天天看點

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

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。