Дизайн, орієнтований на дані - недоцільно з більш ніж 1-2 членами структури?


23

Звичайний приклад дизайну, орієнтованого на дані, має структуру кулі:

struct Ball
{
  float Radius;
  float XYZ[3];
};

а потім вони складають деякий алгоритм, який ітералізує std::vector<Ball>вектор.

Тоді вони дають вам те саме, але реалізовано в дизайні, орієнтованому на дані:

struct Balls
{
  std::vector<float> Radiuses;
  std::vector<XYZ[3]> XYZs;
};

Що добре, і все, якщо ви збираєтесь перейти спочатку через усі радіуси, потім усі позиції тощо. Однак як рухати кульки у векторі? У початковій версії, якщо у вас є std::vector<Ball> BallsAll, ви можете просто перемістити будь-яку BallsAll[x]до будь-якої BallsAll[y].

Однак, щоб зробити це для версії, орієнтованої на дані, ви повинні зробити те саме для кожної властивості (2 рази у випадку кулі - радіус і положення). Але стає гірше, якщо у вас набагато більше властивостей. Вам доведеться зберігати індекс для кожної «кулі», і коли ви намагаєтеся перемістити її, ви повинні робити рух у кожному векторі властивостей.

Хіба це не вбиває ніякої користі для продуктово орієнтованого дизайну?

Відповіді:


23

Ще одна відповідь дала чудовий огляд того, як ви добре інкапсулюєте сховище, орієнтоване на рядки та надайте кращий огляд. Але оскільки ви також запитуєте про продуктивність, дозвольте мені звернутися до наступного питання: Розкладка SoA - це не срібна куля . Це досить непоганий за замовчуванням (для використання кешу; не стільки для зручності реалізації на більшості мов), але це не все є навіть у дизайні, орієнтованому на дані (все, що саме означає). Можливо, що автори деяких прочитаних вами вступів пропустили цю точку та представили лише макет SoA, оскільки вони думають, що це вся суть DOD. Вони помиляються, і, на щастя, не всі потрапляють у цю пастку .

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

Однак, є друга сторона DOD. Ви не отримуєте всіх переваг кешу та організації, просто повернувши макет пам’яті на 90 ° і роблячи щонайменше, щоб виправити отримані помилки компіляції. Існують і інші поширені хитрощі, які викладаються під цим банером. Наприклад, "обробка на основі існування": Якщо ви часто деактивуєте кулі та повторно активуєте їх, не додайте прапор до об'єкта з кулькою та не робіть циклу оновлення ігноруйте кулі з прапорцем, встановленим на false. Перемістіть кульку з "активної" колекції в "неактивну" колекцію, а цикл оновлення лише огляне "колектив".

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

  • Не потрібно перетасовувати колекцію (найшвидший код - це зовсім не код).
  • Ви можете додавати та видаляти простіше та ефективніше (поміняти їх до кінця, остання крапка).
  • Решта код може стати придатним для подальших оптимізацій (наприклад, зміни макета, на якому ви зосереджуєтесь).

Тому замість того, щоб наосліп кидати SoA на все, подумайте про свої дані та про те, як ви їх обробляєте. Якщо ви виявите, що ви обробляєте позиції та швидкості в одному циклі, тоді пройдіть через сітки, а потім оновіть точки відліку, спробуйте розділити макет пам’яті на ці три частини. Якщо ви виявите, що ви отримуєте доступ до компонентів x, y, z позиції поодиноко, можливо, перетворіть свої вектори позиції в SoA. Якщо вам здається, що перетасування даних більше, ніж насправді щось корисне, можливо, перестаньте перемішувати їх.


18

Орієнтований на дані розум

Орієнтований на дані дизайн не означає застосовувати SoAs скрізь. Це просто означає проектування архітектури з переважним акцентом на представлення даних, зокрема з акцентом на ефективне розташування пам’яті та доступ до пам’яті.

Це, можливо, призведе до повторень SoA, коли це доречно:

struct BallSoa
{
   vector<float> x;        // size n
   vector<float> y;        // size n
   vector<float> z;        // size n
   vector<float> r;        // size n
};

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

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

struct BallAoS
{
    float x;
    float y;
    float z;
    float r;
};
vector<BallAoS> balls;        // size n

... в інших випадках може бути доречним використання гібриду, який врівноважує обидві переваги:

struct BallAoSoA
{
    float x[8];
    float y[8];
    float z[8];
    float r[8];
};
vector<BallAoSoA> balls;      // size n/8

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

struct BallAoSoA16
{
    Float16 x2[16];
    Float16 y2[16];
    Float16 z2[16];
    Float16 r2[16];
};
vector<BallAoSoA16> balls;    // size n/16

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

struct BallAoSoA16Hot
{
    Float16 x2[16];
    Float16 y2[16];
    Float16 z2[16];
};
vector<BallAoSoA16Hot> balls;     // size n/16: hot fields
vector<Float16> ball_radiuses;    // size n: cold fields

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

Це приділяє прожектор уваги моделям доступу до пам’яті та супровідним макетам, роблячи їх значно більшу стурбованість, ніж зазвичай. У певному сенсі це може навіть дещо зруйнувати абстракції. Я виявив, застосувавши цей спосіб мислення більше, на що я більше не дивлюся std::deque, наприклад, з точки зору його алгоритмічних вимог настільки ж, як сукупне представлення суміжних блоків, яке воно має, і як працює випадковий доступ до нього на рівні пам'яті. Це дещо ставить акцент на деталі реалізації, але деталі реалізації, які, як правило, мають такий же чи більший вплив на продуктивність, як алгоритмічна складність, що описує масштабованість.

Передчасна оптимізація

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

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

Орієнтований на дані дизайн може залишити приміщення для дихання для вивчення більш ефективних уявлень. Йдеться не про те, щоб досягти досконалості компонування пам’яті за один раз, а більше про те, щоб заздалегідь зробити відповідні міркування, щоб забезпечити все більш оптимальні уявлення.

Гранульований об'єктно-орієнтований дизайн

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

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

Як перебільшений приклад, уявіть об'єктно-орієнтований дизайнерський настрій, застосований до одного пікселя зображення.

class Pixel
{
public:
    // Pixel operations to blend, multiply, add, blur, etc.

private:
    Image* image;          // back pointer to access adjacent pixels
    unsigned char rgba[4];
};

Сподіваємось, ніхто насправді цього не робить. Щоб зробити приклад справді грубим, я зберігав задній вказівник на зображення, що містить піксель, щоб воно могло отримати доступ до сусідніх пікселів для алгоритмів обробки зображень, як розмиття.

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

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

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

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

За допомогою моделювання на рівні об’ємного зображення ми маємо значно більше можливостей для оптимізації. Наприклад, ми можемо представляти великі зображення як об'єднані плитки розміром 16x16 пікселів, які ідеально вписуються в кеш-лінію 64 байтів, але дозволяють ефективно сусідній вертикальний доступ до пікселів із типово малим кроком (якщо у нас є кілька алгоритмів обробки зображень, які потрібно отримати доступ до сусідніх пікселів вертикально) як приклад, орієнтований на жорсткі дані.

Проектування на більш грубому рівні

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

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

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

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

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


5

Те, що ви описали, - це проблема впровадження. OO дизайн явно не стосується реалізації.

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

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

Ефективне додавання / видалення елементів може бути досягнуто за допомогою інших методик:

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

Код клієнта побачив би послідовність об'єктів Ball, змінений контейнер з об'єктами Ball, послідовність радіусів, матриця Nx3 тощо; вона не повинна стосуватися потворних деталей цих складних (але ефективних) структур. Ось що ви купуєте об'єкт абстракції.


+1 AoS-організація цілком підходить до приємного інтерфейсу, орієнтованого на сутність, хоча, звичайно, стає неприємніше ( ball->do_something();проти ball_table.do_something(ball)), якщо ви не хочете підробити цілісну сутність за допомогою псевдопоказника (&ball_table, index).

1
Я піду на крок далі: висновок про використання SoA можна зробити виключно з принципів проектування ОО. Хитрість полягає в тому, що вам потрібен сценарій, коли стовпці є більш фундаментальним об'єктом, ніж рядки. Кульки тут не є хорошим прикладом. Натомість розгляньте місцевість з різними властивостями, такими як висота, тип ґрунту чи кількість опадів. Кожне властивість моделюється як об’єкт ScalarField, у якого є свої методи, такі як градієнт () або дивергенція (), які можуть повертати інші об'єкти поля. Ви можете інкапсулювати такі речі, як роздільна здатність карти, а різні властивості на місцевості можуть працювати з різною роздільною здатністю.
16807,

4

Коротка відповідь: ви цілком правильні, і такі статті, як ця , повністю пропускають цю точку.

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

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

Більшість сучасних мов OO використовують макет пам'яті "Array-Of-Struct" для об'єктів та класів. Отримання переваг OO (наприклад, створення абстракцій для ваших даних, інкапсуляція та більше локального обсягу основних функцій), як правило, пов'язане з таким видом пам’яті. Тому поки ви не займаєтесь обчисленнями високої продуктивності, я б не вважав SoA основним підходом.


3
DOD не завжди означає макет "Структура масиву" (SoA). Це звичайне явище, оскільки воно часто відповідає шаблону доступу, але коли інший макет працює краще, усіляко використовуйте це. DOD - набагато більш загальний (і нечіткий), більше схожий на парадигму дизайну, ніж на певний спосіб викладати дані. Крім того, хоча стаття, на яку ви посилаєтесь, далеко не найкращий ресурс і має свої недоліки, вона не рекламує макети SoA. "А" і "В" можуть бути повністю представлені Ballтак само добре, як і вони можуть бути окремими floats або vec3s (які самі підлягатимуть трансформації SoA).

2
... і дизайн, орієнтований на ряд, який ви згадуєте, завжди міститься в DOD. Це називається масив структур (AoS), і відмінність того, що більшість ресурсів називає "шлях OOP" (для кращого чи іншого), полягає не в розташуванні рядків проти стовпців, а просто в тому, як цей макет відображається в пам'яті (багато невеликих об'єктів пов'язані через покажчики проти великої суцільної таблиці всіх записів). Підводячи підсумок, -1 тому, що хоча ви піднімаєте хороші бали проти помилок ОП, ви неправильно представляєте весь джаз DOD, а не виправляєте розуміння ОП щодо DOD.

@delnan: дякую за ваш коментар, ви, напевно, вірно сказали, що я повинен був використовувати термін "SoA" замість "DOD". Я відповідно відредагував свою відповідь.
Док Браун

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

І цей github.com/BSVino/JaiPrimer/blob/master/JaiPrimer.md, який має автоматичне перетворення з SoA в / з AoS Приклад: reddit.com/r/rust/comments/2t6xqz/…, і ось ось це: новини. ycombinator.com/item?id=10235766
Джеррі Єремія
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.