unity要学ecs吗_ECS的泛泛之谈
這篇文章將帶著你從設(shè)計(jì)出發(fā)重新發(fā)現(xiàn)ECS。
注意:此篇為泛泛之談,不涉及具體實(shí)現(xiàn)。
從Abstract說起
對(duì)對(duì)象的抽象是整理代碼的要點(diǎn),繼承是一種比較古老并常見的抽象,其描述了一個(gè)對(duì)象"是"什么,其中包含了對(duì)象擁有的屬性和對(duì)象擁有的方法,在簡單情況下,繼承是一種非常易用易懂的抽象,然而在更復(fù)雜的情況下,繼承引入的的問題漸漸浮現(xiàn)出來,使得它不再那么易用。
以下列舉幾個(gè)例子:
- 深層次繼承樹(要理解一個(gè)類,需要往上翻看非常多的類)。
- 強(qiáng)耦合(修改基類會(huì)影響到整棵子繼承樹)。
- 菱形繼承(祖父的數(shù)據(jù)重復(fù),方法產(chǎn)生二義性)。
- 繁重的父類(子類的方法被不斷提取到父類,導(dǎo)致父類過度膨脹,某 UE4)。
- 而這些問題又相互影響產(chǎn)生惡性循環(huán),使得項(xiàng)目的后期開發(fā)和優(yōu)化變得無比困難。
于是,大家便嘗試簡化模型,并描述了一種叫做接口的抽象,其描述了一個(gè)對(duì)象"能"干什么,其中包含了對(duì)象擁有的方法(不再包含數(shù)據(jù)),接口隱藏了對(duì)象的大部分細(xì)節(jié),使得對(duì)象變成一個(gè)黑箱,且展平了類結(jié)構(gòu)(不再是樹狀),然而接口(這里指運(yùn)行時(shí)接口而非泛型)作為一種非常高層次的抽象,這種抽象層次似乎有時(shí)會(huì)過高,導(dǎo)致CPU更難以理解代碼,這點(diǎn)在稍后會(huì)討論到。
類似的,在游戲開發(fā)中,面對(duì)大量的對(duì)象種類,大家又描述了一種組件的抽象,如 UE4 中的 Actor Component 模型和 Unity 中的 Entity Component 模型,其描述了一個(gè)對(duì)象"有"什么部分,其中對(duì)象本身不再擁有代碼或數(shù)據(jù)(但其實(shí) Unity 和 UE4 之類的并沒有做到這么純粹,對(duì)象本身依然帶有大量"基礎(chǔ)"功能,這導(dǎo)致了代碼量和內(nèi)存占用的雙重膨脹)。組件的方式帶來了優(yōu)越的動(dòng)態(tài)性,對(duì)象的狀態(tài)完全由其擁有的組件決定(同樣,一般沒這么純粹),甚至可以動(dòng)態(tài)的改變。并且這讓我們可以排列組合以少量的組件組合出巨量的對(duì)象(當(dāng)然,有效組合往往沒那么多)。有趣的是,從展平對(duì)象結(jié)構(gòu)的角度看起來和組件和接口有著微妙的相似性。不過這種抽象也帶來出了一些歧義性,接下來將討論這一點(diǎn)。
組合2. “有”和”能”和實(shí)現(xiàn)
在組件模型中,對(duì)象由組件組成,所以其行為也由組件主導(dǎo),例如一個(gè)對(duì)象擁有[Movement] 和 [Location],則我們可以認(rèn)為它能夠移動(dòng),這在整體上是十分和諧自然的,但當(dāng)我們仔細(xì)考量,這個(gè)"能"是由于什么呢,是因?yàn)?[Movement]嗎,是因?yàn)閇Location]嗎,還是同時(shí)因?yàn)?[Movement] 和 [Location]?當(dāng)然是同時(shí)(這里便揭示出了組件和接口的展平對(duì)象方式是正交的),那移動(dòng)的邏輯放到哪呢?答案是放在這個(gè)“切片“上。但在實(shí)際項(xiàng)目中會(huì)看到把邏輯放在 [Movement] 上的做法,這兩種方式都是可取的,后一種擁有較為簡單的實(shí)現(xiàn)并被廣泛采用,而前一種擁有更精準(zhǔn)的語義,更好的抽象(后一種種方式中 [Movement] 去訪問并修改了 [Location] 的數(shù)據(jù),這破壞了一定的封閉性,且形成了耦合,當(dāng)然這種耦合也有一定的好處,如避免只添加了 [Movement] 這種無意義的情況發(fā)生)。
從Cache說起
Cache(Cache Memory)作為儲(chǔ)存器子系統(tǒng)的組成部分,存放著程序經(jīng)常使用的指令和數(shù)據(jù),是為了緩解訪問慢設(shè)備的延時(shí)而預(yù)讀的 Buffer,例如 CPU L1/L2/L3 Cache 作為 DDR 內(nèi)存 IO 的 Cache,而 DDR 內(nèi)存作為磁盤 IO 的 Cache。當(dāng)計(jì)算需要讀取數(shù)據(jù)的時(shí)候,通常從最快得緩存開始依次向下查找,并遞歸的讀取。預(yù)讀就是用來減少下一次讀取的查找層數(shù)(每一層的延遲有數(shù)量級(jí)的差距)的技術(shù)。相應(yīng)的,預(yù)讀的預(yù)測失敗的時(shí)候?qū)?huì)有非常高的代價(jià),這種情況被稱為 Cache Miss。在大部分的情況下,在現(xiàn)代 CPU 的頻率帶來的運(yùn)算力下, Cache Miss 比數(shù)學(xué)運(yùn)算更容易成為程序的性能瓶頸,且在代碼中的表現(xiàn)比較隱晦。這使得一味的討論復(fù)雜度O(n)不再適用,因?yàn)楝F(xiàn)在效率=數(shù)據(jù)+代碼,最常見的例子就是在數(shù)據(jù)量小的情況下遍歷數(shù)組會(huì)比 (Hash)Map 快上很多,這也是Java或C#這類語言的效率陷阱.。
從上到下進(jìn)行查找2. Avoid Cache Miss
避免 Cache Miss 的方案當(dāng)然就是去討好預(yù)讀。而一般預(yù)讀的策略為線性預(yù)讀,即我們應(yīng)該盡量的保證數(shù)據(jù)讀寫的連續(xù)性,從逆向思維出發(fā),則需了解會(huì)打斷數(shù)據(jù)連續(xù)性的情形。簡單的列舉幾個(gè):遍歷大結(jié)構(gòu)體的數(shù)組(卻只訪問少數(shù)成員),操作對(duì)象引用(OOP),操作數(shù)組的順序不夠連續(xù)(比如實(shí)現(xiàn)得不好的 hash 表),etc。綜上所述,避免Cache Miss的主要考量就是盡量使用數(shù)組,盡量分割屬性(SOA),盡量連續(xù)的進(jìn)行處理。(在 GPU 編程中存在大量實(shí)例)
此時(shí)達(dá)到理論最高效率3. More than Data
前面提到過 Cache 存放著程序經(jīng)常使用的指令和數(shù)據(jù),現(xiàn)代 CPU 在數(shù)據(jù) IO 的時(shí)候并不會(huì)完全的掛起,而是會(huì)利用空閑的運(yùn)算力繼續(xù)執(zhí)行后續(xù)的指令,且指令也是一種數(shù)據(jù),這意味著我們不光要照顧數(shù)據(jù)的連續(xù)性,還需要考慮到指令的連續(xù)性,那么什么情況會(huì)破壞指令的連續(xù)性呢?可能是函數(shù)指針(虛函數(shù)的調(diào)用,回調(diào)等),循環(huán)超長代碼塊等。特別是函數(shù)指針在 IO 期間,CPU 無事可做,于是在需要高性能的情形下,應(yīng)該盡量避免虛函數(shù)。
4. Allocation
對(duì)于數(shù)據(jù)而言,還有一個(gè)重要的問題就是分配內(nèi)存。在應(yīng)用中,不管是分配還是釋放都是十分消耗性能的操作,前者可能產(chǎn)生碎片,而后者,(考慮 GC)可能帶來停頓,(考慮 RC)帶來析構(gòu)血崩,(考慮手動(dòng))也可能帶來危險(xiǎn)和腦力負(fù)擔(dān),所以一般對(duì)于高頻分配的部分,會(huì)預(yù)先分配大塊內(nèi)存用來管理(一般稱作池化)。
從 Thread 說起
隨著處理器核心的發(fā)展速度減緩,為了進(jìn)一步提升處理器的性能,堆疊核心成為了新的出路,甚至現(xiàn)在的處理器沒個(gè)四核都不好意思見人,其中堆疊核心的巔峰就是 GPU,上千個(gè)核心帶來了瘋狂的數(shù)字處理能力,被廣泛運(yùn)用于 AI 和圖形領(lǐng)域。而這在游戲之類的高性能軟件中,為了充分利用 CPU 的算力,程序設(shè)計(jì)成多線程運(yùn)行也是非常必要的。
2. Race Condition 和 Data Race
不幸的是多線程很多時(shí)候不是免費(fèi)的性能,并不是所有情況都像異步讀文件那么簡單,在開發(fā)過程中,很多地方都可能會(huì)有 Race 的發(fā)生。同步性問題非常的惡心,因?yàn)橥ǔF洳粫?huì)即時(shí)造成崩潰之類的錯(cuò)誤,而是會(huì)積累錯(cuò)誤,等到錯(cuò)誤爆發(fā),緣由已經(jīng)很難查詢。所以編碼的時(shí)候就必須要小心翼翼,其中 Race Condition 主要需要我們保證整體操作的原子性,一般的解決方案是一把大鎖。Data Race 則更加復(fù)雜,觸發(fā)Data Race的條件可以歸納為:
1,同一個(gè)位置的對(duì)象。
2,被兩個(gè)并行的線程操作。
3,兩個(gè)線程并非都是讀。
4,不是原子操作。
只有當(dāng)這四個(gè)條件同時(shí)成立的時(shí)候,Data Race 才會(huì)發(fā)生,所以為了避免它的發(fā)生,我們需要破壞掉其中的一個(gè)或多個(gè)條件。對(duì)于條件4,可以使用原子操作破壞,然而原子操作的復(fù)雜性頗高,實(shí)際應(yīng)用中常用于實(shí)現(xiàn)底層庫(無鎖隊(duì)列,線程池之類的)。而要破壞條件1、3,則是避免可變共享,完全進(jìn)行拷貝(如erlang)。剩下條件2就是避免硬碰硬,在可能發(fā)生 Data Race 的時(shí)候直接放棄并行。但總得來說最重要的還是,要避免它的發(fā)生,一定要對(duì)這些條件足夠敏感以預(yù)防遺漏,在這里通常封裝就起了反作用,因?yàn)楹谙渲畠?nèi)我們無法知道會(huì)發(fā)生什么。而此時(shí)相對(duì)于 OOP 的黑箱,函數(shù)式的純粹(純原子性)便能體現(xiàn)出它在并行上天生的優(yōu)勢,所以卡神推薦在 C++ 里也盡量使用函數(shù)式的思想來進(jìn)行編碼。
交匯之地 - 三相之力!
之前說到組件模式的時(shí)候,我們列舉了兩種方式來存放實(shí)現(xiàn)組件功能的代碼,而使用“管理器”實(shí)現(xiàn)的方式,擁有更精準(zhǔn)的語義和更好的抽象,組件之間被徹底解耦,而這個(gè)“管理器”我們稱之為系統(tǒng)(System)。即系統(tǒng)負(fù)責(zé)管理特定的組件的組合,而組件則不再負(fù)責(zé)邏輯。接下來分別討論這兩個(gè)部分。
篩選對(duì)應(yīng)的實(shí)體2. System
對(duì)象耦合于接口,而這里系統(tǒng)則耦合于對(duì)象。這意味著組件不變的情況下,系統(tǒng)的任何修改都不會(huì)對(duì)程序的其余部分造成影響。這給代碼帶來了出色的內(nèi)聚性,讓 culling 和 plugin 都變得更輕松,并且系統(tǒng)本身擁有很好的純度,我們完全可以把系統(tǒng)看做是”輸入上一幀的數(shù)據(jù),輸出下一幀的數(shù)據(jù)“。也就是系統(tǒng)本身貼合了函數(shù)式的思想,根據(jù)前面的敘述,函數(shù)式在并行上有天生的優(yōu)勢,這在系統(tǒng)上也體現(xiàn)了出來:系統(tǒng)負(fù)責(zé)管理組件的信息是透明的,于是我們對(duì)系統(tǒng)對(duì)組件的讀寫便一目了然 - 注意結(jié)構(gòu)體之間沒有任何依賴,系統(tǒng)與系統(tǒng)之間的沖突也一目了然。更進(jìn)一步,在通常情況下,系統(tǒng)是一個(gè)白箱,運(yùn)作系統(tǒng)的代碼將不會(huì)經(jīng)過虛函數(shù),不管是效率還是可測試性都是極好的。甚至對(duì)于系統(tǒng)的執(zhí)行調(diào)度也完全暴露了出來,這在實(shí)現(xiàn)網(wǎng)絡(luò)同步之類的框架的時(shí)候能提供很大的便捷性。
3. Component 與 Entity
對(duì)于對(duì)象本身,其實(shí)已經(jīng)不必要承載多少信息了,激進(jìn)一點(diǎn)說,對(duì)象甚至只是一個(gè)唯一的ID,用于和其他對(duì)象區(qū)分而已,這讓我們有機(jī)會(huì)去除那些"基礎(chǔ)"功能的依賴(例如 Transform),使得內(nèi)存和代碼進(jìn)一步壓縮。而組件不包含邏輯,就只有數(shù)據(jù),作為一個(gè)大的對(duì)象的分割的屬性,通常為小結(jié)構(gòu)體。對(duì)于每一種組件,我們可以使用緊密的數(shù)組來儲(chǔ)存它,而這也意味著我們可以輕松的池化這個(gè)數(shù)組。在系統(tǒng)管理組件的時(shí)候,并不關(guān)心特定 Entity,而是在組件數(shù)據(jù)的切片上批量的連續(xù)的進(jìn)行處理,這在理想情況下能大大的減少 Cache Miss 的情況。作為額外的好處,純數(shù)據(jù)的組件對(duì)序列化,表格化有著極強(qiáng)的適應(yīng)性,畢竟對(duì)象天生就是一個(gè)填著組件的表格,對(duì)網(wǎng)絡(luò)、編輯、存檔等都十分的友好。(這里也可以引入很多數(shù)據(jù)庫相關(guān)的知識(shí))
4. ECS
至此,我們重新發(fā)現(xiàn)了 ECS,并詳細(xì)闡述了它的好處。
總結(jié)
以上是生活随笔為你收集整理的unity要学ecs吗_ECS的泛泛之谈的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java读取excel数据的方法是_ja
- 下一篇: figtree如何编辑进化树_iTOL快