Dots初探和原理分析
1.什么是DOTS
DOTS是Data-Oriented Tech Stack的縮寫,中文名:多線程數(shù)據(jù)導(dǎo)向型技術(shù)堆棧
DOTS由以下3部分內(nèi)容構(gòu)成
ECS Entity Component System
C# Job System
Burst Compiler
Dots的官方數(shù)據(jù)
450w個(gè)mesh render
10萬(wàn)個(gè)音頻文件同時(shí)播放
5k個(gè)不同的汽車模型
20萬(wàn)個(gè)不同的建筑模型
穩(wěn)定60幀
DOTS原理
- ECS 用于編寫高性能代碼
- Job System 用于高效運(yùn)行多線程代碼
- Burst Compiler 用于生成高度優(yōu)化的本地代碼
這三者是完全不同的兩個(gè)概念,可以分開(kāi)使用,但結(jié)合使用才能發(fā)揮最大優(yōu)勢(shì)
- ECS: 緩存友好,利用struct去掉GC
- JOBS:充分利用多線程
- BURST:SIMD,LLVM 編譯優(yōu)化
了解完DOTS的概念定義,接著來(lái)剖析一下為什么DOTS能帶來(lái)如此顯著的性能提升.
首先我們需要知道緩存友好和局部性原理:
緩存友好
CPU緩存是什么?
CPU緩存的定義為CPU與內(nèi)存之間的臨時(shí)數(shù)據(jù)交換器,它的出現(xiàn)是為了解決CPU運(yùn)行處理速度與內(nèi)存讀寫速度不匹配的矛盾——緩存的速度比內(nèi)存的速度快很多。CPU緩存一般直接跟CPU芯片集成或位于主板總線互連的獨(dú)立芯片上。(現(xiàn)階段的CPU緩存一般直接集成在CPU上)CPU往往需要重復(fù)處理相同的數(shù)據(jù)、重復(fù)執(zhí)行相同的指令,如果這部分?jǐn)?shù)據(jù)、指令CPU能在CPU緩存中找到,CPU就不需要從內(nèi)存或硬盤中再讀取數(shù)據(jù)、指令,從而減少了整機(jī)的響應(yīng)時(shí)間。
- CPU訪問(wèn)緩存的原理
下面這張圖是cpu內(nèi)核和各級(jí)緩存的概念圖,距離內(nèi)核越近的速度越快,容量越小,距離內(nèi)核越遠(yuǎn)的速度越慢容量越大.
下圖為真實(shí)CPU內(nèi)部結(jié)構(gòu)
局部性原理
時(shí)間局部性:在一個(gè)具有良好的時(shí)間局部性的程序當(dāng)中,被引用過(guò)一次的內(nèi)存位置,在將來(lái)一個(gè)不久的時(shí)間內(nèi)很可能會(huì)被再次引用到。
空間局部性:在一個(gè)具有良好的空間局部性的程序當(dāng)中,一個(gè)內(nèi)存位置被引用了一次,那么在不久的時(shí)間內(nèi)很可能會(huì)引用附近的位置。
根據(jù)空間局部性原理,CPU把上述內(nèi)容放到緩存里面不會(huì)只放幾個(gè)字節(jié),一般會(huì)將所在的物理頁(yè)內(nèi)容都加載到緩存,例如4KB
如何提高CPU緩存命中率
-
什么是緩存行
CPU緩存由緩存行構(gòu)成,每個(gè)緩存行占用64字節(jié) -
緩存行是怎么被浪費(fèi)掉的
數(shù)據(jù)浪費(fèi):假設(shè)需要移動(dòng)一個(gè)物體的x軸方向的坐標(biāo),傳統(tǒng)的做法是修改物體的localPosition,但localPosition中還包含有y和z,雖然我們并不需要修改y和z,但是由于最小單位就是一個(gè)Vector3,因此多余的y和z就白白占用了寶貴的緩存,造成了浪費(fèi).
多余組件浪費(fèi):當(dāng)我們需要修改某個(gè)GameObject的坐標(biāo)時(shí),一定是得到該GameObject本身,再獲取其transform組件,再修改此transform組件的localPosition,雖然最終和transform沒(méi)什么關(guān)系,但卻不得不緩存此transform組件,造成浪費(fèi).
ECS和普通數(shù)據(jù)存儲(chǔ)之間的區(qū)別
ECS要求所有相同的組件在內(nèi)存中都排列在一起
Chunk和Archetype
ECS中的數(shù)據(jù)以Chunk為單位進(jìn)行數(shù)據(jù)排列,相同結(jié)構(gòu)的Entity會(huì)被放到同一個(gè)類型的Chunk中.
多個(gè)Chunk組成Archetype.
ECS運(yùn)作示意圖
Entity身上掛載了Component,多個(gè)不同的Component數(shù)據(jù)交由System進(jìn)行運(yùn)算,運(yùn)算結(jié)果再以Component的形式返還給Entity,完成一輪循環(huán).
Entity和普通類對(duì)象
- 不能使用繼承進(jìn)行擴(kuò)展
- 設(shè)計(jì)模式的更新,嚴(yán)格使用組合替代繼承
把GameObject轉(zhuǎn)Entity
unity官方提供了把傳統(tǒng)GameObject轉(zhuǎn)換為Entity的接口,但經(jīng)實(shí)際測(cè)試,轉(zhuǎn)換效率低,轉(zhuǎn)換5000個(gè)GameObject需要數(shù)秒時(shí)間,不具備商用價(jià)值,建議直接創(chuàng)建Entity,再往其身上掛載Component,雖然繁雜了一點(diǎn),但是運(yùn)行效率能得到保障.
System生命周期
2.JobSystem
JobSystem和常見(jiàn)的多線程編程沒(méi)有太大的區(qū)別,具體原理不再贅述.
JobSystem是整個(gè)DOTS中理解成本較低,但使用成本較高的部分.
unity包攬了幾乎所有多線程調(diào)度,工作壓力自動(dòng)分配等傳統(tǒng)多線程的工作,我們只需要按照unity定好的規(guī)則和接口進(jìn)行套用就可以了,理解上幾乎沒(méi)有額外成本,但由于必須按照unity定好的接口來(lái)進(jìn)行編碼,同時(shí)需要記憶不少接口,因此使用成本略高,加上最近的幾個(gè)版本多線程方面的接口不停在調(diào)整和增減,因此也增加了不少使用成本.
3.Burst編譯器
Burst編譯器是由開(kāi)源LLVM二次開(kāi)發(fā)所得,由于Unity必須兼顧大量的跨平臺(tái)特性,同時(shí)LLVM也確實(shí)是個(gè)優(yōu)秀的編譯器,因此untiy選擇基于LLVM來(lái)開(kāi)發(fā)自己的編譯器.
說(shuō)道Burst編譯器我們必須先了解一下SIMD指令優(yōu)化
SIMD是什么
SIMD全稱Single Instruction Multiple Data,單指令多數(shù)據(jù)流,能夠讀取多個(gè)操作數(shù),并把它們打包在大型寄存器的一組指令集。一次獲取多個(gè)操作數(shù)后,存放于一個(gè)大型寄存器,再進(jìn)行運(yùn)算,從而達(dá)到一條指令完成對(duì)多個(gè)對(duì)象計(jì)算的效果,實(shí)現(xiàn)加速。
SIMD可一次執(zhí)行多條命令
Unity新數(shù)學(xué)庫(kù)
unity引入了全新的數(shù)學(xué)計(jì)算庫(kù):Mathematics
Mathematics使用了SIMD并行計(jì)算
由于3D圖形學(xué)中,涉及到大量的矩陣運(yùn)算,因此SIMD剛好可以充分發(fā)揮其效用,大大提升并行運(yùn)算的速度.
如何使用burst編譯器
使用Burst編譯器幾乎是傻瓜式的,只需要在函數(shù)定義前加上如下申明即可:
[BurstCompile]下圖為C#到機(jī)器碼的過(guò)程
unity官方提供能了Burst編譯器的處理流讓大家看到具體發(fā)生了什么事情:
Burst編譯效果對(duì)比
.Net Core比c++慢2倍
mono比.Net Core慢3倍
IL2CPP比mono快2~3倍
IL->LLVM IR比.Net Core快3倍
HPC#
C# chass類型數(shù)據(jù)的內(nèi)存分配在堆上,無(wú)法通過(guò)代碼主動(dòng)釋放,必須等到.NET垃圾回收才可真正清理
IL2CPP雖然將IL轉(zhuǎn)換成C++代碼,但垃圾回收使用的Boehm(貝姆),所以效率并非等同于c++
HPC#就是NativeArray可代替數(shù)組T[]數(shù)據(jù)類型包括值類型(float,int,uint,short,bool),enums,structs和其他類型的指針
NativeArray,NativeList系列API可以在C#層分配C++中的對(duì)象,可以主動(dòng)釋放不需要進(jìn)行C#的垃圾回收
JobSystem中使用的就是NativeArray
總結(jié):ECS+JobSystem+burst+SIMD+hpc#造就了DOTS如此夸張的運(yùn)行效率
4.Hybird Renderer
由于傳統(tǒng)的模型,貼圖,動(dòng)畫(huà)等并不能在ECS架構(gòu)下正常運(yùn)行,因此Unity另外開(kāi)發(fā)了一套專門適配ECS的渲染系統(tǒng)Hybird Renderer
不過(guò)當(dāng)下的Hybird Renderer模式仍然很不完美,不支持動(dòng)畫(huà)系統(tǒng)+蒙皮動(dòng)畫(huà),只支持簡(jiǎn)單的縮放和角度調(diào)整,個(gè)人覺(jué)得還沒(méi)達(dá)到商用的程度.
5.如何在已有項(xiàng)目中嵌入dots
目前整個(gè)DOTS在不停的迭代中,整個(gè)ECS及JOBS已相對(duì)成熟,市面上已有部分公司在商業(yè)項(xiàng)目中進(jìn)行了探索和嘗試,但DOTS在渲染部分仍然有很多缺陷,因此可以利用傳統(tǒng)的動(dòng)畫(huà)和蒙皮組件來(lái)制作動(dòng)畫(huà),使用DOTS來(lái)替代原本MonoBehaviour來(lái)進(jìn)行高強(qiáng)度的運(yùn)算,得到運(yùn)算結(jié)果后傳遞給傳統(tǒng)GameObject,由GameObject來(lái)進(jìn)行呈現(xiàn).
6.如何安裝dots
Dots可在PackageManager中進(jìn)行安裝,本文章是基于下面的版本進(jìn)行的學(xué)習(xí)和探索:
目前網(wǎng)上有部分DOTS的學(xué)習(xí)示例工程,但由于Packge Manager的緣故,很多工程并未標(biāo)明所使用的DOTS版本,導(dǎo)致大部分工程都無(wú)法正常運(yùn)行,這里提一個(gè)小tips:
使用所下載的工程所用的Unity版本來(lái)打開(kāi)工程,而不是自己電腦上當(dāng)前所使用的Unity版本,這樣在版本匹配的前提下,Unity能自動(dòng)匹配之前所使用的DOTS版本,這也是我在學(xué)習(xí)的過(guò)程中摸索出來(lái)的小竅門.
7.Dots適合的使用場(chǎng)景
Dots在游戲開(kāi)發(fā)上能提供超乎想象的運(yùn)行效率,但是并不是所有游戲都適合使用Dots,目前Dots更適合有大量單位,并且每個(gè)單位需要進(jìn)行大量雷同運(yùn)算的場(chǎng)景,如果是不太追求運(yùn)算效率的簡(jiǎn)單游戲,使用DOTS反而使得整個(gè)架構(gòu)變得不必要的復(fù)雜,維護(hù)成本也會(huì)同時(shí)變高.
8.DOTS Demo展示:
如下demo分別用傳統(tǒng)的MonoBehaviour,ECS,ECS+JOBS 3種方式對(duì)同樣的邏輯運(yùn)算進(jìn)行了測(cè)試,可以很明確的區(qū)分出3者之間顯著的性能差別
https://github.com/CraneInForest/DOTS_SAMPLE.git總結(jié)
以上是生活随笔為你收集整理的Dots初探和原理分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: cisco IOS,nexus和Aris
- 下一篇: 苹果手机投影_手机和投影同时用流量能否投