Як отримати вигоду з кеш-процесора в системному двигуні компонентної системи сутності?


15

Я часто читаю в документаціях на ігровий механізм ECS, що є гарною архітектурою для розумного використання кеш-пам'яті.

Але я не можу зрозуміти, як ми можемо отримати вигоду з кеш-процесора.

Якщо компоненти зберігаються в масиві (або пулі), у постійній пам'яті, це хороший спосіб використовувати кеш-процесор, АЛЕ, якщо ми читаємо компоненти послідовно.

Коли ми використовуємо системи, їм потрібен перелік об'єктів, який є переліком об'єктів, які мають компоненти з певними типами.

Але ці списки надають компоненти випадковим чином, а не послідовно.

Тож як створити ECS для максимального збільшення кешу?

Редагувати:

Наприклад, фізичній системі потрібен список сутностей для сутності, що має компоненти RigidBody та Transform (Є пул для RigidBody та пул для компонентів Transform).

Тож його цикл для оновлення об'єктів буде таким:

for (Entity eid in entitiesList) {
    // Get rigid body component
    RigidBody *rigidBody = entityManager.getComponentFromEntity<RigidBody>(eid);

    // Get transform component
    Transform *transform = entityManager.getComponentFromEntity<Transform>(eid);

    // Do something with rigid body and transform component
}

Проблема полягає в тому, що компонент RigidBody entiteta1 може знаходитись в індексі 2 свого пулу, а компонент Tranform сутності1 в індексі 0 свого пулу (тому що деякі об'єкти можуть мати деякі компоненти, а не інші, і через додавання / видалення сутності / компоненти довільно).

Тож навіть якщо компоненти несумісні в пам'яті, вони зчитуються випадковим чином, і це матиме більше пропусків кешу, ні?

Якщо тільки не існує способу попереднього вибору наступних компонентів у циклі?


чи можете ви показати нам, як ви розподіляєте кожен компонент?
concept3d

За допомогою простого розподільника пулу та диспетчера Handle, який має довідник компонентів для управління переміщенням компонентів у пулі (для збереження постійних компонентів у пам'яті).
Джонмф

Ваш приклад циклу передбачає, що оновлення компонентів перемежовуються в сутність. У багатьох випадках можливо оновити компоненти масово за типом компонентів (наприклад, спочатку оновіть усі компоненти жорсткого тіла, потім оновіть усі перетворення готовими даними жорстких тіл, потім оновіть усі дані рендерінгу новими перетвореннями ...) - це може покращити кеш використовувати для оновлення кожного компонента. Я думаю, що такий тип структури - це те, що Нік Віггілл пропонує нижче.
DMGregory

Це мій приклад, який поганий, насправді це більше "оновлення всіх перетворень готовими жорсткими даними про тіло", ніж фізична система. Але проблема залишається тією ж, що в цих системах (оновлення перетворення з жорстким тілом, надання оновлення з перетворенням, ...) нам потрібно буде мати більше одного типу компонентів одночасно.
Джонмф

Не впевнені, чи це теж може бути актуально? gamasutra.com/view/feature/6345/…
DMGregory

Відповіді:


13

Стаття Міка Веста пояснює процес лінеаризації даних компонентів сутності. Багато років тому він працював над серією "Тоні Хоук" на набагато менш вражаючому обладнання, ніж у нас сьогодні, щоб значно покращити продуктивність. В основному він використовував глобальні, попередньо виділені масиви для кожного окремого типу даних сутності (позиція, оцінка та багато чого) та посилається на кожен масив у окрему фазу його загальносистемної update()функції. Можна припустити, що дані для кожної сутності будуть мати однаковий індекс масиву в кожному з цих глобальних масивів, так, наприклад, якщо плеєр створений першим, він може мати свої дані [0]у кожному масиві.

Ще більш специфічні для оптимізації кешу, Christer Ericsson слайди для C та C ++.

Щоб детальніше ознайомитись, вам слід спробувати використовувати суміжні блоки пам'яті (найлегше виділяються як масиви) для кожного типу даних (наприклад, позиція, xy та z), щоб забезпечити хорошу локальність посилань, використовуючи кожен такий блок даних чітко. update()фази заради тимчасової локалізації, тобто для того, щоб кеш не був очищений за допомогою апаратного алгоритму LRU до того, як ви використали будь-які дані, які ви збираєтесь повторно використовувати, у межах даного update()дзвінка. Як ви вже мали на увазі, те, що ви не хочете робити, - це виділяти ваші сутності та компоненти як дискретні об'єкти через new, оскільки дані різних типів про кожен екземпляр сутності потім будуть переплетені, зменшуючи локальність посилань.

Якщо у вас взаємозалежності між компонентами (даними) такі, що ви абсолютно не можете дозволити собі, щоб окремі дані були відокремлені від пов’язаних даних (наприклад, Transform + Physics, Transform + Renderer), ви можете вибрати тиражувати дані Transform як у масивах Physics, так і в Renderer. , переконуючись, що всі відповідні дані відповідають ширині лінії кешу для кожної критичної для виконання операції.

Пам'ятайте також, що кеш L2 і L3 (якщо ви можете припустити їх для своєї цільової платформи) роблять багато для полегшення проблем, які можуть зазнати кеш-пам’яті L1, наприклад, обмежувальної ширини рядка. Тож навіть у випадку пропуску L1, це безпечні мережі, які найчастіше запобігають виклику головної пам’яті, яка на порядок повільніше, ніж опис будь-якого рівня кешу.

Примітка щодо запису даних Писання не викликає основну пам'ять. За замовчуванням у сьогоднішніх системах увімкнено кешування зворотного запису: запис значення записує його лише в кеш (спочатку), а не в основну пам'ять, тому ви не будете обмежені цим. Лише тоді, коли запитуються дані з основної пам'яті (не відбудеться, поки вони знаходяться в кеші), і основна пам'ять буде оновлена ​​з кеша.


1
Лише примітка для всіх, хто може бути новим у C ++: std::vectorце, в основному, масив, що динамічно змінює розмір, і, отже, є суміжним (фактично у старих версіях C ++ та де-юре в нових версіях C ++). Деякі реалізації std::dequeтакож є "суміжними" (хоча не Microsoft).
Шон Міддлічч

2
@Johnmph Просто: Якщо у вас немає місцеположення, у вас нічого немає. Якщо два фрагменти даних тісно пов’язані між собою (наприклад, просторова та фізична інформація), тобто вони обробляються разом, то, можливо, вам доведеться їх ущільнити як єдиний компонент, переплетений. Але майте на увазі, що будь-яка інша логіка (скажімо, AI), яка використовує ці просторові дані, може потім постраждати внаслідок невключення просторових даних поряд з ними . Тож це залежить від того, що вимагає найбільшої продуктивності (можливо, фізика у вашому випадку). Чи має це сенс?
Інженер

1
@Johnmph так, я повністю погоджуюсь з Ніком, це про те, як вони зберігаються в пам'яті, якщо у вас є об'єкт із вказівниками на два компоненти, які знаходяться далеко в пам'яті, у вас немає локальності, вони повинні вміщуватися в кеш-рядок.
concept3d

2
@Johnmph: Дійсно, стаття Міка Веста передбачає мінімальні взаємозалежності. Отже: мінімізуйте залежності; Реплікація даних по рядках кеша , де ви не можете звести до мінімуму цієї залежності ... наприклад , включають в себе перетворення разом як RigidBody і візуалізації; і щоб підходити до рядків кеша, можливо, вам доведеться максимально зменшити атоми даних ... цього можна частково досягти, перейшовши від плаваючої точки до фіксованої точки (4 байти проти 2 байтів) за значення десяткової крапки. Але так чи інакше, незалежно від того, як ви це зробите, ваші дані повинні відповідати ширині лінії кешу, як зазначалося concept3d, для досягнення максимальної продуктивності.
Інженер

2
@Johnmph. Ні. Щоразу, коли ви пишете дані Transform, ви просто записуєте їх до обох масивів. Це не ті записи, про яких потрібно хвилюватися. Після того, як ви відправте повідомлення, це так само добре, як і зроблено. Це показання , пізніше в оновленнях, коли ви запускаєте Physics and Renderer, які повинні мати доступ до всіх відповідних даних негайно, в одному рядку кешу, прямо впритул і особисто до CPU. Крім того, якщо вам дійсно потрібно все це разом, то ви або робите подальші реплікації, або переконайтеся, що фізика, перетворення та візуалізація підходять до однієї лінії кешу ... 64 байти є загальним і насправді досить багато даних! ...
Інженер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.