Ефективне розділення кроків читання / обчислення / запису для паралельної обробки сутності в системах Entity / Component


11

Налаштування

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

Entity
{
    id;
    map<id_type, Attribute> attributes;
}

System
{
    update();
    vector<Entity> entities;
}

Можливо, система, яка просто рухається по всіх об'єктах з постійною швидкістю

MovementSystem extends System
{
   update()
   {
      for each entity in entities
        position = entity.attributes["position"];
        position += vec3(1,1,1);
   }
}

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

Проблема

У випадку показаної системи MovementS паралелізація є тривіальною. Оскільки сутності не залежать один від одного і не змінюють спільних даних, ми можемо просто переміщувати всі об'єкти паралельно.

Однак ці системи іноді вимагають, щоб суб'єкти взаємодіяли (читали / записували дані з / в) один з одним, іноді в межах однієї системи, але часто між різними системами, які залежать один від одного.

Наприклад, у фізичній системі іноді сутності можуть взаємодіяти один з одним. Два об'єкти стикаються, з них зчитуються їхні позиції, швидкості та інші атрибути, оновлюються, а потім оновлені атрибути списуються до обох сутностей.

І перш ніж система візуалізації в двигуні може почати візуалізацію об'єктів, потрібно дочекатися завершення виконання інших систем, щоб переконатися, що всі відповідні атрибути є такими, якими вони повинні бути.

Якщо ми спробуємо сліпо паралелізувати це, це призведе до класичних умов перегонів, коли різні системи можуть читати та змінювати дані одночасно.

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

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

Рішення?

На мій погляд, можливим рішенням була б система, де читання / оновлення та запис даних розділено, так що на одній дорогій фазі системи лише зчитують дані та обчислюють те, що потрібно для обчислення, якось кешують результати, а потім записують усі змінені дані повертаються цільовим об'єктам в окремий пропуск. Усі системи діятимуть на дані в тому стані, в якому вони знаходилися на початку кадру, а потім до кінця кадру, коли всі системи закінчуються оновленням, відбувається серіалізований пропуск для запису, де кешується результат усіх різних системи повторюються і записуються до цільових об'єктів.

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

Питання

Як така система може бути впроваджена для досягнення оптимальних показників? Які деталі реалізації такої системи та які передумови для системи Entity-Component, яка хоче використовувати це рішення?

Відповіді:


1

----- (на основі переглянутого питання)

Перший момент: оскільки ви не згадуєте про профілактику виконання версії для випуску і знайшли певну потребу, я пропоную вам зробити це якомога швидше. Як виглядає ваш профіль, чи ви стираєте кеші з поганим розташуванням пам’яті, чи є одне серцевина на 100%, скільки відносного часу витрачається на обробку вашого ECS порівняно з рештою вашого двигуна тощо?

Читайте від сутності та щось обчислюйте ... і дотримуйтесь результатів десь у проміжній зоні зберігання до пізніше? Я не думаю, що ви можете відокремити читання + обчислити + зберігати так, як ви думаєте, і очікуєте, що цей проміжний магазин буде чимось, крім чистого режиму.

Крім того, оскільки ви робите безперервну обробку, головне правило, яке ви хочете дотримуватися, - це мати один потік на ядро ​​CPU. Я думаю, що ви дивитесь на це неправильний шар , спробуйте переглянути цілі системи, а не окремі сутності.

Створіть графік залежності між вашими системами, дерево того, яка система потребує результатів попередньої роботи системи. Коли у вас є це дерево залежності, ви зможете легко відправити цілі системи, повноцінні сутності, обробляти по потоку.

Тож скажімо, що ваше дерево залежності - це загроза мішалок та пасток для ведмедів, питання дизайну, але ми повинні працювати з тим, що маємо. Найкращим випадком є ​​те, що всередині кожної системи кожна сутність не залежить від будь-якого іншого результату всередині цієї системи. Тут ви легко розділите обробку на потоки, 0-99 та 100-199 на два потоки, наприклад, з двома ядрами та 200 об'єктами, якими володіє ця система.

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

Ідея побудови графіка залежності полягала в тому, щоб реалізувати, здавалося б, неможливе завдання "Пошук та збірка інших систем для паралельної роботи" шляхом автоматизації її. Якщо такий графік показує ознаки блокування постійним очікуванням попередніх результатів, тоді створення читання + модифікація та затримка запису лише переміщує блокування та не видаляє серійний характер обробки.

І послідовну обробку можна повертати лише паралельно між кожною точкою послідовності, але не загальною. Але ви це усвідомлюєте, тому що це суть вашої проблеми. Навіть якщо ви кеш читає дані, які ще не були записані, вам все одно потрібно чекати, коли цей кеш стане доступним.

Якби створити паралельні архітектури було легко або навіть можливо з такими обмеженнями, то інформатика не мала б боротися з проблемою з часів Блетчлі-парку.

Єдиним реальним рішенням було б мінімізувати всі ці залежності, щоб зробити пункти послідовності якомога рідше необхідними. Це може включати підрозділення систем на послідовні етапи обробки, де всередині кожної підсистеми проходження паралельно з потоками стає тривіальним.

Найкраще, що я отримав за цю проблему, і це справді не що інше, як рекомендувати, що якщо вдарити головою об цегляну стіну, болить, то розбийте її на менші цегляні стіни, так що ви тільки б'єте по гомілці.


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

Також не передбачається правопорушення: P
TravisG

@TravisG Часто є системи, які залежать від інших систем, як зазначив Патрік. Щоб уникнути затримок кадру або уникнути декількох пропусків оновлення як частини логічного кроку, прийнятим рішенням є серіалізація фази оновлення, запуску підсистем паралельно, де це можливо, серіалізація підсистем із залежностями, в той час як пакетне зменшення менших оновлень проходить всередині кожної підсистеми, що використовує паралельну_for () концепцію. Це ідеально підходить для будь-якої комбінації потреб підсистеми оновлення підсистеми і найбільш гнучко.
Нарос

0

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

Цей метод також видаляє умови перегонів, оскільки всі системи працюватимуть із усталеним станом, який не змінюватиметься до / після того, як система обробила його.


Це хитрому копію Джона Кармака, чи не так? Я замислювався над цим, але все ще потенційно існує та сама проблема, що кілька потоків можуть записувати на одне вихідне місце. Це, мабуть, хороше рішення, якщо ви будете тримати все "однопрохідним", але я не впевнений, наскільки це можливо.
TravisG

Затримка введення для екранного екрана збільшиться на 1 кадр, включаючи реактивність GUI. Що може мати значення для ігор на дію / синхронізацію чи важких маніпуляцій з графічним інтерфейсом, таких як RTS. Мені це подобається як творча ідея.
Патрік Хьюз

Я чув про це від друга, і не знав, що це фокус Кармака. Залежно від того, як здійснюється візуалізація, візуалізація компонентів може бути на одному кадрі позаду. Ви можете просто використати це для етапу оновлення, а потім зробити його з поточної копії, коли все буде оновлено.
Джон Макдональд

0

Я знаю 3 розробки програмного забезпечення для паралельної обробки даних:

  1. Обробляйте дані послідовно : Це може здатися непосильним, оскільки ми хочемо обробити дані за допомогою декількох потоків. Однак для більшості сценаріїв потрібні кілька потоків саме для того, щоб робота була завершена, а інші потоки чекають або виконують тривалі операції. Найчастіше використовують потоки інтерфейсу користувача, які оновлюють користувальницький інтерфейс в одному потоці, тоді як інші потоки можуть працювати у фоновому режимі, але їм заборонено здійснювати прямий доступ до елементів інтерфейсу. Для передачі результатів з фонових ниток використовуються черги завдань , які будуть оброблені одним потоком при наступній розумній можливості.
  2. Синхронізуйте доступ до даних: це найпоширеніший спосіб обробки декількох потоків, що мають доступ до одних і тих же даних. Більшість мов програмування мають вбудовані класи та інструменти для того, щоб блокувати розділи, де дані читаються та / або записуються кількома потоками одночасно. Однак слід бути обережними, щоб не блокувати операції. З іншого боку, такий підхід коштує великих витрат на додатки в режимі реального часу.
  3. Обробляйте одночасні модифікації лише тоді, коли вони трапляються: такий оптимістичний підхід можна зробити, якщо зіткнення трапляються рідко. Дані будуть прочитані та змінені, якщо взагалі не було багаторазового доступу, але існує механізм, який визначає, коли дані оновлювались одночасно. Якщо це станеться, одинарне обчислення просто буде виконуватися знову до успіху.

Ось кілька прикладів для кожного підходу, який може бути використаний в системі сутності:

  1. Давайте придумаємо, CollisionSystemщо читає Positionта RigidBodyкомпоненти, і слід оновити Velocity. Замість того, щоб Velocityбезпосередньо маніпулювати , CollisionSystemволя замість цього ставить a CollisionEventу чергу роботи an EventSystem. Потім ця подія буде оброблятися послідовно з іншими оновленнями до Velocity.
  2. EntitySystemЦе впливає на доступність компонентів , які потрібно читати і писати. Для кожного Entityвін буде мати блокування читання для кожного компонента, який він хоче прочитати, і блокування запису для кожного компонента, який він хоче оновити. Таким чином, кожен EntitySystemзможе одночасно читати компоненти під час синхронізації операцій оновлення.
  3. Беручи приклад компонента MovementSystem, Positionкомпонент є незмінним і містить ревізійний номер. MovementSystemСавелій читає Positionі Velocityкомпонент і обчислює новий Position, збільшується лічені ревізії номера і намагаються оновлюваних в Positionкомпонент. У разі одночасного внесення змін, фреймворк вказує це на оновлення, і цей файл Entityбуде повернутий до списку об'єктів, які повинні бути оновлені MovementSystem.

Залежно від систем, об'єктів та інтервалів оновлення, кожен підхід може бути добрим чи поганим. Система системної системи може дозволяти користувачеві вибирати між цими параметрами для налаштування продуктивності.

Я сподіваюся, що я міг би додати до дискусії кілька ідей і, будь ласка, повідомте мене, чи є якісь новини про це.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.