In che modo i sistemi di entità sono efficienti nella cache?


32

Ultimamente, ho letto molte cose sui sistemi di entità da implementare nel mio motore di gioco C ++ / OpenGL. I due principali vantaggi che sento costantemente lodati sui sistemi di entità sono

  1. la facile costruzione di nuovi tipi di entità, dovuta al fatto di non dover intrecciarsi con complesse gerarchie ereditarie, e
  2. efficienza della cache, che sto riscontrando problemi di comprensione.

La teoria è semplice, ovviamente; ogni componente viene archiviato in modo contiguo in un blocco di memoria, quindi il sistema che si prende cura di quel componente può semplicemente scorrere su tutto l'elenco, senza dover saltare in memoria ed eliminare la cache. Il problema è che non riesco davvero a pensare a una situazione in cui questo è effettivamente pratico.


Innanzitutto, diamo un'occhiata a come vengono archiviati i componenti e come fanno riferimento a vicenda. I sistemi devono essere in grado di funzionare con più di un componente, ovvero sia il sistema di rendering che quello fisico devono accedere al componente di trasformazione. Ho visto un numero di possibili implementazioni che affrontano questo problema e nessuno di loro lo fa bene.

È possibile che i componenti memorizzino i puntatori su altri componenti o i puntatori alle entità che memorizzano i puntatori sui componenti. Tuttavia, non appena si gettano i puntatori nel mix, si sta già uccidendo l'efficienza della cache. Puoi assicurarti che ogni array di componenti sia 'n' grande, dove 'n' è il numero di entità attive nel sistema, ma questo approccio è orribilmente dispendioso di memoria; questo rende molto difficile aggiungere nuovi tipi di componenti al motore, ma getta via l'efficienza della cache, perché stai passando da un array al successivo. Potresti intercalare il tuo array di entità, invece di mantenere array separati, ma stai ancora sprecando memoria; rendendo proibitivamente costoso aggiungere nuovi componenti o sistemi, ma ora con l'ulteriore vantaggio di invalidare tutti i vecchi livelli e salvare i file.

Tutto questo presuppone che le entità vengano elaborate linearmente in un elenco, ogni frame o tick. In realtà, questo non è spesso il caso. Supponi di utilizzare un renderizzatore di settore / portale o un ottetto per eseguire l'abbattimento dell'occlusione. Potresti essere in grado di memorizzare le entità contigue all'interno di un settore / nodo, ma stai per saltare, che ti piaccia o no. Quindi hai altri sistemi, che potrebbero preferire le entità memorizzate in un altro ordine. L'intelligenza artificiale potrebbe andare bene con la memorizzazione di entità in un grande elenco, fino a quando non inizi a lavorare con AI LOD; quindi, ti consigliamo di dividere l'elenco in base alla distanza dal giocatore o ad altre metriche LOD. La fisica vorrà usare quell'otto. Agli script non importa, devono funzionare, qualunque cosa accada.

Ho potuto vedere la suddivisione dei componenti tra "logica" (ad es. Ai, script, ecc.) E "mondo" (ad es. Rendering, fisica, audio, ecc.) E gestire ogni elenco separatamente, ma questi elenchi devono ancora interagire tra loro. L'intelligenza artificiale è inutile, se non può influire sulla trasformazione o sullo stato di animazione utilizzato per il rendering di un'entità.


In che modo i sistemi di entità sono "efficienti nella cache" in un motore di gioco reale? Forse esiste un approccio ibrido che tutti usano, ma non parlano, come archiviare entità in un array a livello globale e fare riferimento a esso all'interno dell'ottavo?


Nota che al giorno d'oggi hai CPU multi-core e cache più grande di una riga. Anche se hai bisogno di informazioni di accesso da due sistemi, è probabile che si adattino a entrambi. Inoltre, il rendering grafico è spesso separato - esattamente per quello che hai affermato (alberi, scene, ..)
wondra

2
I sistemi di entità non sono sempre efficienti in termini di cache, ma possono essere un vantaggio di alcune implementazioni (rispetto ad altri modi per ottenere risultati simili).
Josh,

Risposte:


43

I due principali vantaggi che sento costantemente lodati sui sistemi di entità sono 1) la facile costruzione di nuovi tipi di entità a causa del fatto di non dover fare i grovigli con complesse gerarchie ereditarie e 2) l'efficienza della cache.

Si noti che (1) è un vantaggio della progettazione basata su componenti , non solo ES / ECS. Puoi usare i componenti in molti modi che non hanno la parte "sistemi" e funzionano bene (e molti giochi indie e AAA usano tali architetture).

Il modello di oggetti Unity standard (utilizzo GameObjecte MonoBehaviouroggetti) non è un ECS, ma è una progettazione basata su componenti. La più recente funzionalità di Unity ECS è un vero ECS, ovviamente.

I sistemi devono essere in grado di funzionare con più di un componente, vale a dire sia il sistema di rendering che quello fisico devono accedere al componente di trasformazione.

Alcuni ECS ordinano i loro contenitori di componenti in base all'ID entità, il che significa che i componenti corrispondenti in ciascun gruppo saranno nello stesso ordine.

Ciò significa che se stai iterando linearmente sui componenti grafici, stai anche iterando linearmente sui corrispondenti componenti di trasformazione. Potresti saltare alcune delle trasformazioni (dal momento che potresti avere volumi di trigger fisici che non esegui il rendering o simili) ma dal momento che salti sempre in memoria (e di solito non con distanze particolarmente grandi) stai ancora andando avere guadagni di efficienza.

Questo è simile al modo in cui la struttura degli array (SOA) è l'approccio consigliato per HPC. La CPU e la cache sono in grado di gestire più array lineari quasi allo stesso modo in cui possono gestire un singolo array lineare e molto meglio di quanto possano gestire l'accesso casuale alla memoria.

Un'altra strategia utilizzata in alcune implementazioni di ECS, incluso Unity ECS, è quella di allocare i componenti in base all'archetipo della loro entità corrispondente. In altre parole, tutte le Entità con esattamente l'insieme di Componenti ( PhysicsBody, Transform) verranno allocate separatamente dalle Entità con Componenti diversi (ad es PhysicsBody. Transform, E Renderable ).

I sistemi in tali progetti funzionano individuando innanzitutto tutti gli Archetipi che soddisfano i loro requisiti (che hanno il set richiesto di Componenti), ripetendo tale elenco di Archetipi e ripetendo i Componenti memorizzati all'interno di ciascun Archetipo corrispondente. Ciò consente l'accesso ai componenti O (1) completamente lineare e vero all'interno di un Archetipo e consente a Systems di trovare Entità compatibili con un sovraccarico molto basso (cercando un piccolo elenco di Archetipi piuttosto che cercare potenzialmente centinaia di migliaia di Entità).

È possibile che i componenti memorizzino i puntatori su altri componenti o i puntatori alle entità che memorizzano i puntatori sui componenti.

I componenti che fanno riferimento ad altri componenti sulla stessa entità non devono archiviare nulla. Per fare riferimento a componenti su altre entità, è sufficiente memorizzare l'ID entità.

Se un componente può esistere più di una volta per una singola entità ed è necessario fare riferimento a una particolare istanza, memorizzare l'ID dell'altra entità e un indice del componente per tale entità. Molte implementazioni ECS non consentono questo caso, in particolare perché rende queste operazioni meno efficienti.

Puoi assicurarti che ogni array di componenti sia 'n' grande, dove 'n' è il numero di entità attive nel sistema

Utilizzare le maniglie (ad es. Indici + marcatori di generazione) e non puntatori, quindi è possibile ridimensionare le matrici senza timore di rompere i riferimenti agli oggetti.

Puoi anche usare un approccio di "chunked array" (un array di array) simile a molte std::dequeimplementazioni comuni (anche se senza le dimensioni pietosamente piccole di tali implementazioni) se vuoi consentire i puntatori per qualche motivo o se hai misurato problemi con prestazioni di ridimensionamento dell'array.

In secondo luogo, tutto ciò presuppone che le entità vengano elaborate linearmente in un elenco ogni frame / tick, ma in realtà ciò non accade spesso

Dipende dall'entità. Sì, per molti casi d'uso non è vero. In effetti, questo è il motivo per cui sottolineo così fortemente la differenza tra la progettazione basata su componenti (buona) e il sistema di entità (una forma specifica di CBD).

Alcuni dei tuoi componenti saranno sicuramente facili da elaborare in modo lineare. Anche in casi d'uso normalmente "ad albero pesante" abbiamo sicuramente visto un aumento delle prestazioni dall'uso di array strettamente compressi (soprattutto nei casi che coinvolgono una N di alcune centinaia al massimo, come gli agenti di intelligenza artificiale in un gioco tipico).

Alcuni sviluppatori hanno anche scoperto che i vantaggi in termini di prestazioni derivanti dall'uso di strutture di dati allocate linearmente orientate ai dati superano il vantaggio in termini di prestazioni derivante dall'utilizzo di strutture basate su alberi "più intelligenti". Dipende tutto dal gioco e dai casi d'uso specifici, ovviamente.

Supponi di utilizzare un renderizzatore di settore / portale o un ottetto per eseguire l'abbattimento dell'occlusione. Potresti essere in grado di memorizzare entità contigue all'interno di un settore / nodo, ma salterai in giro, che ti piaccia o no.

Saresti sorpreso di quanto l'array aiuti ancora. Stai saltando in una regione di memoria molto più piccola di "ovunque" e anche con tutto il salto hai ancora più probabilità di finire in qualcosa nella cache. Con un albero di una certa dimensione o meno, potresti persino essere in grado di precaricare tutto nella cache e non perdere mai la cache su quell'albero.

Ci sono anche strutture ad albero che sono costruite per vivere in matrici strette. Ad esempio, con il tuo octree, puoi utilizzare una struttura simile a un heap (genitori prima dei figli, fratelli vicini uno accanto all'altro) e assicurarti che anche quando "esegui il drill down" dell'albero stai sempre iterando in avanti nell'array, il che aiuta la CPU ottimizza gli accessi alla memoria / ricerche nella cache.

Questo è un punto importante da sottolineare. Una CPU x86 è una bestia complessa. La CPU sta effettivamente eseguendo un ottimizzatore di microcodici sul codice della tua macchina, suddividendolo in microcodici più piccoli e istruzioni di riordino, predicendo schemi di accesso alla memoria, ecc. I modelli di accesso ai dati contano più di quanto possa essere facilmente evidente se tutto ciò che hai è una comprensione di alto livello di come funziona la CPU o la cache.

Quindi hai altri sistemi, che potrebbero preferire entità memorizzate in un altro ordine.

Potresti memorizzarli più volte. Una volta ridotti gli array ai minimi dettagli, potresti trovare effettivamente un risparmio di memoria (poiché hai rimosso i puntatori a 64 bit e puoi utilizzare indici più piccoli) con questo approccio.

È possibile interlacciare il proprio array di entità invece di mantenere array separati, ma si sta ancora sprecando memoria

Questo è antitetico al buon utilizzo della cache. Se tutto ciò che ti interessa sono le trasformazioni e i dati grafici, perché fare in modo che la macchina passi il tempo a raccogliere tutti gli altri dati per la fisica e l'intelligenza artificiale, l'input, il debug e così via?

Questo è il punto di solito sottolineato a favore degli oggetti di gioco ECS vs monolitici (anche se non realmente applicabili se confrontati con altre architetture basate su componenti).

Per quello che vale, la maggior parte delle implementazioni ECS "di livello di produzione" di cui sono a conoscenza utilizzano lo storage interfogliato. Il popolare approccio Archetype che ho citato in precedenza (utilizzato in Unity ECS, ad esempio) è stato esplicitamente creato per utilizzare lo storage interlacciato per i componenti associati a un Archetype.

L'intelligenza artificiale è inutile se non può influire sulla trasformazione o sullo stato di animazione utilizzato per il rendering di un'entità.

Solo perché l'IA non può accedere in modo efficiente ai dati di trasformazione in modo lineare, ciò non significa che nessun altro sistema può utilizzare efficacemente l'ottimizzazione del layout dei dati. È possibile utilizzare un array compresso per trasformare i dati senza impedire ai sistemi di logica di gioco di fare le cose nel modo in cui i sistemi di logica di gioco di solito fanno le cose.

Dimentichi anche la cache del codice . Quando usi l'approccio sistemico di ECS (a differenza di un'architettura più ingenua dei componenti) stai garantendo che stai eseguendo lo stesso piccolo ciclo di codice e non saltando avanti e indietro attraverso le tabelle delle funzioni virtuali per un assortimento di Updatefunzioni casuali sparse ovunque il tuo binario. Quindi nel caso dell'IA, vuoi davvero conservare tutti i tuoi diversi componenti dell'IA (perché sicuramente ne hai più di uno in modo da poter comporre comportamenti!) In secchi separati ed elaborare ogni elenco separatamente al fine di ottenere il miglior utilizzo della cache del codice.

Con una coda di eventi ritardata (in cui un sistema genera un elenco di eventi ma non li invia fino a quando il sistema non termina l'elaborazione di tutte le entità) è possibile assicurarsi che la cache del codice venga utilizzata correttamente mantenendo gli eventi.

Utilizzando un approccio in cui ciascun sistema sa quali code di eventi leggere per il frame, è anche possibile velocizzare la lettura degli eventi. O più veloce che senza, almeno.

Ricorda, le prestazioni non sono assolute. Non è necessario eliminare ogni ultima perdita della cache per iniziare a vedere i vantaggi in termini di prestazioni di una buona progettazione orientata ai dati.

C'è ancora una ricerca attiva per far funzionare meglio molti sistemi di gioco con l'architettura ECS e modelli di progettazione orientati ai dati. Analogamente ad alcune delle cose straordinarie che abbiamo visto fare con SIMD negli ultimi anni (ad esempio i parser JSON), stiamo vedendo sempre più cose fatte con l'architettura ECS che non sembra intuitivo per le architetture di gioco classiche ma offre una serie di vantaggi (velocità, multi-threading, testabilità, ecc.).

O forse c'è un approccio ibrido che tutti usano ma nessuno sta parlando

Questo è ciò che ho sostenuto in passato, in particolare per le persone che sono scettiche sull'architettura ECS: utilizzare buoni approcci orientati ai dati per i componenti in cui le prestazioni sono fondamentali. Usa un'architettura più semplice in cui la semplicità migliora i tempi di sviluppo. Non calpestare ogni singolo componente in una rigida sovra-definizione di componentizzazione come propone ECS. Sviluppa la tua architettura di componente in modo tale da poter facilmente utilizzare approcci simili a ECS laddove abbiano senso e utilizzare una struttura di componenti più semplice in cui l'approccio simile a ECS non ha senso (o ha meno senso di una struttura ad albero o così via) .

Personalmente sono un convertito relativamente recente al vero potere dell'ECS. Anche se per me, il fattore decisivo era qualcosa di raramente menzionato su ECS: rende quasi banali i test di scrittura per i sistemi di gioco e la logica rispetto ai progetti basati su componenti carichi di logica strettamente accoppiati con cui ho lavorato in passato. Dato che le architetture ECS mettono tutta la logica nei sistemi, che consumano solo componenti e producono aggiornamenti dei componenti, creare un insieme "finto" di componenti per testare il comportamento del sistema è abbastanza semplice; poiché la maggior parte della logica di gioco dovrebbe vivere esclusivamente all'interno dei sistemi, ciò significa che testare tutti i sistemi fornirà una copertura del codice abbastanza elevata della logica di gioco. I sistemi possono utilizzare dipendenze simulate (ad es. Interfacce GPU) per test con una complessità o un impatto sulle prestazioni di gran lunga inferiori rispetto a te "

A parte, potresti notare che molte persone parlano di ECS senza capire davvero di cosa si tratti. Vedo il classico Unity indicato come ECS con una frequenza deprimente, a dimostrazione del fatto che troppi sviluppatori di giochi identificano "ECS" con "Componenti" e praticamente ignorano del tutto la parte "Entity System". Vedi un sacco di amore accumulato su ECS su Internet quando una grande parte della gente sta davvero sostenendo la progettazione basata su componenti, non un vero ECS. A questo punto è quasi inutile discuterne; ECS è stato corrotto dal suo significato originale in un termine generico e potresti anche accettare che "ECS" non significa la stessa cosa di "ECS orientato ai dati". : /


1
Sarebbe utile definire (o collegarsi a) cosa intendi per ECS, se hai intenzione di confrontarlo / contrastarlo con una progettazione generale basata su componenti. Io per primo non sono chiaro su quale sia la distinzione. :)
Nathan Reed il

Grazie mille per la risposta, sembra che ho ancora molte ricerche da fare sull'argomento. Ci sono dei libri a cui potresti indicarmi?
Haydn V. Harach,

3
@NathanReed: ECS è documentato in luoghi come entity-systems.wikidot.com/es-terminology . Il design basato sui componenti è solo una normale aggregazione-sopra-eredità, ma con l'accento sulla composizione dinamica utile per il design del gioco. Puoi scrivere motori basati su componenti che non usano Sistemi o Entità (nel significato della terminologia ECS) e puoi usare componenti per molto più in un motore di gioco che solo oggetti / entità di gioco, motivo per cui sottolineo la differenza.
Sean Middleditch,

2
Questo è uno dei migliori post sugli ECS che abbia mai letto, nonostante tutta la letteratura sul web. Mega pollice in alto. Quindi, Sean, in definitiva, qual è il tuo approccio generale allo sviluppo di giochi (piuttosto quelli complessi)? Un ECS puro? Un approccio misto tra componente e ECS? Mi piacerebbe sapere di più sui tuoi progetti! Ti sta chiedendo troppo per metterti in contatto con Skype o qualcos'altro per discuterne?
Grimshaw,

2
@Grimshaw: gamedev.net è un posto decente per discussioni più aperte, come direi reddit.com/r/gamedev (anche se io non sono un rediter). Sono spesso su gamedev.net, così come molte altre persone brillanti. In genere non faccio conversazioni individuali; Sono piuttosto impegnato e preferisco spendere i miei tempi di inattività (ovvero la compilazione) aiutando molti piuttosto che pochi. :)
Sean Middleditch l'
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.