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