Ce este proiectarea motorului de date orientat spre date?

Este posibil să fi auzit de proiectarea motorului de joc orientată spre date, un concept relativ nou care propune o mentalitate diferită față de designul tradițional orientat spre obiect. În acest articol, vă voi explica ce se referă la DOD și de ce unii dezvoltatori de motoare de joc consideră că ar putea fi biletul pentru câștiguri spectaculoase de performanță.

Un pic de istorie

În primii ani de dezvoltare a jocului, jocurile și motoarele lor au fost scrise în limbi de școală veche, cum ar fi C. Au fost un produs de nișă și stoarcerea ultimului ciclu de ceas din hardware lent era, în acea perioadă, prioritate maximă. În majoritatea cazurilor, un număr limitat de oameni au fost hackeri la codul unui singur titlu și au știut cu toată inima codul de bază. Instrumentele pe care le foloseau îi serviseră bine, iar C asiguraa avantajele de performanță care le-au permis să împingă maxim din procesor - și întrucât aceste jocuri erau încă legate de CPU, făcându-și propriile tampoane de cadre, acesta a fost un punct foarte important.

Odată cu apariția unităților de procesare grafică (GPU) care fac lucrarea de răsfoire a numerelor pe triunghiuri, texele, pixeli și așa mai departe, am ajuns să depindem mai puțin de CPU. În același timp, industria jocurilor de noroc a înregistrat o creștere constantă: tot mai mulți oameni doresc să joace tot mai multe jocuri, iar acest lucru la rândul său a dus la tot mai multe echipe care se reunesc pentru a le dezvolta. 

Legea lui Moore arată că creșterea hardware-ului este exponențială, nu liniară în ceea ce privește timpul: aceasta înseamnă că la fiecare doi ani, numărul de tranzistori pe care îl putem plasa pe o singură placă nu se schimbă cu o sumă constantă - se dublează!

Echipele mai mari au avut nevoie de o mai bună cooperare. Nu mai mult timp, motoarele de joc, cu nivelul lor complex, AI, culegerea și logica de redare, necesitau coderi mai disciplinați și arma lor de a alege proiectare orientată pe obiecte.

Așa cum Paul Graham a spus odată: 

La marile companii, software-ul tinde să fie scris de echipe mari (și care se schimbă frecvent) de programatori mediocru. Programarea orientată pe obiecte impune o disciplină acestor programatori, care împiedică pe oricare dintre ei să facă pagube prea mari.

Fie că ne place sau nu, acest lucru trebuie să fie adevărat într-o oarecare măsură - companiile mai mari au început să desfășoare jocuri mai mari și mai bune, iar odată cu standardizarea instrumentelor, hackerii care lucrau la jocuri deveniseră părți care ar fi putut fi schimbate mai ușor. Virtutea unui hacker a devenit din ce în ce mai puțin importantă.

Probleme cu design obiect-orientat

În timp ce designul orientat pe obiecte este un concept frumos care ajută dezvoltatorii pe proiecte mari, cum ar fi jocurile, să creeze mai multe straturi de abstractizare și să aibă toată lumea să lucreze pe stratul țintă, fără să aibă grijă de detaliile de implementare ale celor dedesubt, dă-ne niste dureri de cap.

Vedem o explozie de programe de codare paralele care recoltează toate nucleele de procesoare disponibile pentru a furniza viteze de calcul blazive - dar în același timp, peisajul de joc devine din ce în ce mai complex și dacă vrem să ținem pasul cu această tendință și să livrăm cadrele -pentru a doua oară jucătorii noștri se așteaptă, trebuie să o facem și noi. Utilizând toată viteza pe care o avem la dispoziție, putem deschide ușile pentru posibilități complet noi: folosind timpul procesorului pentru a reduce numărul de date trimise GPU-ului, de exemplu.

În programarea orientată pe obiecte, păstrați starea într-un obiect, care vă cere să introduceți concepte ca primitivele de sincronizare dacă doriți să lucrați pe ele din mai multe fire. Aveți un nou nivel de indirecție pentru fiecare apel virtual pe care îl efectuați. Și modelele de acces la memorie generate de codul scris într-un mod orientat pe obiect poate sa să fie îngrozitor - de fapt, Mike Acton (Jocuri Insomniac, ex-Rockstar Games) are un set mare de diapozitive explicând ocazional un exemplu. 

În mod similar, Robert Harper, profesor la Universitatea Carnegie Mellon, a spus: 

Programarea orientată spre obiect este [...] atât anti-modulară, cât și anti-paralelă prin însăși natura sa și, prin urmare, neadecvată pentru un curriculum moderne al CS.

Vorbind despre acest POP, acest lucru este dificil, deoarece OOP cuprinde un spectru larg de proprietăți, și nu toți sunt de acord cu ce înseamnă POR. În acest sens, vorbesc cel mai mult despre OOP, așa cum este implementat de C ++, pentru că în prezent acesta este limbajul care domină foarte mult lumea motoarelor de jocuri.

Deci, știm că jocurile trebuie să devină paralele, deoarece există întotdeauna mai multă muncă pe care CPU o poate (dar nu trebuie să o facă), și ciclurile de cheltuieli care așteaptă să proceseze unitatea de procesare grafică sunt doar risipitoare. Știm, de asemenea, că abordările comune de proiectare OO necesită introducerea unor constrângeri costisitoare de blocare și, în același timp, pot să încalce localitatea cache sau să provoace ramificații inutile (care pot fi costisitoare!) În cele mai neașteptate circumstanțe.

Dacă nu profităm de mai multe nuclee, vom continua să folosim aceeași cantitate de resurse CPU, chiar dacă hardware-ul devine în mod arbitrar mai bun (are mai multe nuclee). În același timp, putem împinge GPU-ul la limitele sale deoarece este, prin design, paralel și capabil să preia orice cantitate de lucru simultan. Acest lucru poate interfera cu misiunea noastră de a oferi jucătorilor cea mai bună experiență în ceea ce privește hardware-ul lor, deoarece în mod evident nu îl folosim la potențialul maxim.

Aceasta ridică întrebarea: ar trebui să ne regândim paradigmele cu totul?

Introduceți: Design orientat spre date

Unii susținători ai acestei metodologii au denumit design orientat spre date, dar adevărul este că conceptul general a fost cunoscut mult mai mult. Premisa sa de baza este simpla: construiți codul în jurul structurilor de date și descrieți ce doriți să obțineți în ceea ce privește manipularea acestor structuri

Am auzit o astfel de discuție: Linus Torvalds, creatorul Linux și Git, a declarat într-un post de pe lista de discuții Git că este un susținător uriaș în "proiectarea codului în jurul datelor, nu invers" și cred că acesta este unul dintre motivele succesului lui Git. El continuă chiar să pretindă că diferența dintre un programator bun și unul rău este dacă îi îngrijorează structurile de date sau codul în sine.

Sarcina poate părea contraintuitivă la început, pentru că vă cere să vă transformați modelul mental în sus. Dar gândiți-vă în acest fel: un joc, în timp ce rulează, captează toate intrările utilizatorului și toate piesele grele de performanță ale acestuia (cele în care ar fi logic să scape standardul totul este un obiect filosofia) nu se bazează pe factori externi, cum ar fi rețeaua sau IPC. Pentru tot ce știi, un joc consumă evenimentele utilizatorilor (mouse-ul mutat, butonul joystick-ului apăsat și așa mai departe) și starea actuală a jocului și le transformă într-un nou set de date - de exemplu, loturi care sunt trimise GPU- Eșantioane PCM care sunt trimise pe placa audio și o stare nouă a jocului.

Această "churning de date" poate fi împărțită în mai multe subprocese. Un sistem de animație are următoarele date cheie cheie și starea actuală și produce o stare nouă. Un sistem de particule își ia starea actuală (pozițiile particulelor, vitezele și așa mai departe) și o avansare în timp și produce o stare nouă. Un algoritm de sacrificare are un set de variabile candidate și produce un set mai restrâns de renderables. Aproape totul într-un motor de joc poate fi considerat ca manipulând o bucată de date pentru a produce o altă bucată de date.

Procesoarele iubesc localitatea de referință și utilizarea cache-ului. Deci, în proiectarea orientată spre date, avem tendința de a organiza, ori de câte ori este posibil, totul în matrice mari, omogene și, ori de câte ori este posibil, rulați algoritmi de forță brute, coerenți în coajă cache, în locul unui potențial fanciar (care are mai bine costul Big O, dar nu reușește să îmbrățișeze limitările arhitecturii hardware pe care funcționează). 

Când este realizat pe cadru (sau de mai multe ori pe cadru), acest lucru oferă potențiale recompense uriașe. De exemplu, cei de la Scalyr raportează căutând fișierele jurnal la 20GB / sec folosind o scanare liniară de forță brute-forată. 

Când procesăm obiecte, trebuie să ne gândim la ele ca la "cutii negre" și să le numim metodele, care la rândul lor accesează datele și ne obțin ce vrem (sau facem schimbările pe care le dorim). Acest lucru este minunat pentru lucrul pentru mentenabilitate, dar neștiind cum sunt prezentate datele noastre poate fi în detrimentul performanței.

Exemple

Proiectarea orientată spre date ne face să ne gândim totul la date, așa că hai să facem și ceva diferit de ceea ce facem de obicei. Luați în considerare această bucată de cod:

void MyEngine :: queueRendables () pentru (auto it = mRenderables.begin (); it! = mRenderables.end (); ++ it) if ((it)) -> isVisible ()) queueRenderable ); 

Deși simplificată foarte mult, acest model comun este ceea ce se vede adesea în motoarele de joc orientate pe obiecte. Dar așteptați - dacă o mulțime de renderables nu sunt de fapt vizibile, ne confruntăm cu o mulțime de predicții greșite ramură care determină procesorul să smulg niște instrucțiuni pe care le-a executat în speranța că a fost luată o anumită ramură. 

Pentru scenele mici, acest lucru nu este, evident, o problemă. Dar de câte ori faceți acest lucru deosebit, nu doar atunci când așteptați în așteptare, dar când iterați prin lumini de scenă, se împrăștie hărți de umbre, zone sau altele asemănătoare? Ce zici de AI sau actualizări de animație? Înmulțiți tot ce faceți în întreaga scenă, vedeți câte cicluri de ceasuri expulzați, calculați cât timp procesorul dvs. are disponibil pentru a livra toate loturile GPU pentru un ritm constant de 120FPS și vedeți că aceste lucruri poate sa scară la o sumă considerabilă. 

Ar fi amuzant dacă, de exemplu, un hacker care lucrează la o aplicație web chiar a luat în considerare astfel de micro-optimizări minuscule, dar știm că jocurile sunt sisteme în timp real în care constrângerile resurselor sunt incredibil de strânse, deci această considerație nu este nereușită pentru noi.

Pentru a evita acest lucru, să ne gândim la aceasta într-un alt mod: dacă păstrăm lista de renderables vizibile în motor? Sigur, ne-ar sacrifica sintaxa curată a myRenerable-> ascunde () și încalcă câteva principii OOP, dar am putea face acest lucru:

void MyEngine :: queueRendabile () pentru (auto it = mVisibleRenderables.begin (); it! = mVisibleRenderables.end (); ++ it) queueRenderable (* it); 

Ura! Nu există nicio eroare în favoarea ramurii și presupunând mVisibleRenderables este frumos std :: vector (care este o matrice contiguă), am fi putut rescrie atât de repede acest lucru ca un repede memcpy apel (cu câteva actualizări suplimentare pentru structurile noastre de date, probabil).

Acum, poți să mă suni pe marginea brută a acestor mostre de cod și vei fi destul de corect: acest lucru este simplificat mult. Dar, pentru a fi sincer, încă nu am zgâriat încă suprafața. Gândirea asupra structurilor de date și a relațiilor lor ne deschide la o mulțime de posibilități pe care nu le-am mai gândit până acum. Să ne uităm la următorii.

Paralelizare și vectorizare

Dacă avem funcții simple, bine definite, care funcționează pe blocuri mari de date ca blocuri de bază pentru procesarea noastră, este ușor să reproducem patru sau opt sau 16 fire de lucrători și să le dăm câte o bucată de date pentru a păstra toate procesoarele coresele ocupate. Nu există mutexuri, atomi sau controverse de blocare și, odată ce ai nevoie de date, trebuie doar să te alături tuturor firelor și să aștepți ca ei să termine. Dacă aveți nevoie să sortați date în paralel (o sarcină foarte frecventă atunci când pregătiți lucruri care trebuie trimise la GPU), trebuie să vă gândiți la acest lucru dintr-o perspectivă diferită - aceste diapozitive ar putea ajuta.

Ca un bonus suplimentar, în interiorul unui fir puteți folosi instrucțiuni vectoriale SIMD (cum ar fi SSE / SSE2 / SSE3) pentru a obține o creștere suplimentară a vitezei. Uneori, puteți realiza acest lucru numai prin plasarea datelor într-un mod diferit, cum ar fi plasarea unor vectori într-o manieră de structură-array (SoA) (cum ar fi XXX ... YYY ... ZZZ ... ), mai degrabă decât array-of-structuri convenționale (AoS, care ar fi XYZXYZXYZ ... ). Abia zgâriez suprafața; puteți găsi mai multe informații în Citirea în continuare mai jos.

Atunci când algoritmii noștri se ocupă de date direct, devine trivial să le paralelize și putem, de asemenea, să evităm unele neajunsuri de viteză.

Testarea unităților pe care nu știați că a fost posibilă

Funcțiile simple, fără efecte externe, le ușurează testul pe unitate. Acest lucru poate fi deosebit de bun într-o formă de teste de regresie pentru algoritmi pe care doriți să le schimbați ușor și ușor. 

De exemplu, puteți construi o suită de testare pentru un comportament al algoritmului de sacrificare, puteți configura un mediu orchestrat și puteți măsura exact modul în care acesta efectuează. Când creați un nou algoritm de deșertificare, executați din nou același test, fără modificări. Puteți măsura performanța și corectitudinea, astfel încât să puteți avea evaluarea la îndemână. 

Pe măsură ce ajungeți mai mult în abordările de design orientate spre date, veți găsi mai ușor și mai ușor să testați aspectele motorului dvs. de joc.

Combinând clasele și obiectele cu date monolitice

Design-ul orientat spre date nu se opune programării orientate pe obiecte, ci doar câteva dintre ideile sale. Ca rezultat, puteți folosi cu ușurință idei de la designul orientat spre date și obțineți tot mai multe abstracții și modele mentale cu care sunteți obișnuit. 

Uitați-vă, de exemplu, la lucrul la versiunea 2.0 a OGRE: Matias Goldberg, creatorul acestui proces, a ales să stocheze date în matrice mari și omogene și să aibă funcții care să reproducă întreaga rețea, spre deosebire de un singur punct , pentru a accelera Ogre. Potrivit unui punct de referință (pe care îl recunoaște este foarte nedrept, dar avantajul de performanță măsurat nu poate fi numai din cauza asta) funcționează acum de trei ori mai rapid. Nu numai asta - el a păstrat multe dintre abstracțiile de clasă vechi, familiare, astfel încât API era departe de a fi o rescriere completă.

Este practic?

Există o mulțime de dovezi că motoarele de joc în acest mod pot și vor fi dezvoltate.

Blogul de dezvoltare al motorului Molecule are o serie numită Aventuri în proiectarea orientată spre date,și conține o mulțime de sfaturi utile cu privire la locul în care a fost pus DOD cu rezultate extraordinare.

DICE pare să fie interesat de design-ul orientat spre date, deoarece îl foloseau în sistemul de sacrificare al lui Frostbite Engine (și a obținut, de asemenea, progrese semnificative!). Unele alte diapozitive de la acestea includ, de asemenea, utilizarea de proiectare orientate spre date în subsistemul AI - merită să ne uităm și la ele.

În afară de aceasta, dezvoltatorii ca Mike Acton, menționați mai sus, acceptă conceptul. Există câteva puncte de referință care demonstrează că câștigă mult în performanță, dar nu am văzut o mulțime de activități pe frontul de design orientat spre date în ceva timp. Ar putea, desigur, să fie doar o umilință, dar premisele sale principale par foarte logice. Există cu siguranță o mulțime de inerție în această afacere (și orice altă afacere de dezvoltare a software-ului, care contează), astfel încât aceasta ar putea împiedica adoptarea pe scară largă a unei astfel de filozofii. Sau poate nu este o idee atât de minunată cum pare să fie. Tu ce crezi? Comentariile sunt foarte binevenite!

Citirea în continuare

  1. Design-ul orientat pe date (sau de ce ați putea fi împușcat în picioare cu OOP)
  2. Introducere în proiectarea orientată spre date [DICE] 
  3. O discuție destul de drăguță despre suprapunerile de stive 
  4. O carte online a lui Richard Fabian explicând o mulțime de concepte 
  5. Un punct de referință arătând o altă parte a povestirii, un rezultat aparent contra-intuitiv 
  6. Recenzia lui Mike Acton despre OgreNode.cpp, care dezvăluie unele capcane comune ale dezvoltării motorului OOP