Орієнтований на дані розум
Орієнтований на дані дизайн не означає застосовувати 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 байтів, але дозволяють ефективно сусідній вертикальний доступ до пікселів із типово малим кроком (якщо у нас є кілька алгоритмів обробки зображень, які потрібно отримати доступ до сусідніх пікселів вертикально) як приклад, орієнтований на жорсткі дані.
Проектування на більш грубому рівні
Наведений вище приклад моделювання інтерфейсів на рівні зображення є своєрідним непридатним прикладом, оскільки обробка зображення - це дуже зріле поле, яке вивчалося та оптимізоване до смерті. Однак менш очевидною може бути частинка у випромінювачі частинок, спрайт проти колекції спрайтів, край у графіку країв, або навіть людина проти колекції людей.
Ключ, що дозволяє оптимізації, орієнтованих на дані (в перспективі чи задньому огляді), часто полягає в розробці інтерфейсів на набагато більш грубому рівні. Ідея проектування інтерфейсів для окремих об'єктів замінюється проектуванням для колекцій сутностей з великими операціями, які обробляють їх оптом. Це особливо і негайно націлено на послідовні петлі доступу, які потребують доступу до всього і не можуть допомогти, але мають лінійну складність.
Дизайн, орієнтований на дані, часто починається з ідеї об'єднання даних для формування агрегатів, що моделюють дані масово. Подібний спосіб мислення перегукується з проектами інтерфейсу, які супроводжують його.
Це найцінніший урок, який я взяв із дизайну, орієнтованого на дані, оскільки я не є достатньо підкованим архітектурою, щоб часто знаходити найоптимальніший макет пам'яті для чогось із моєї першої спроби. Це стає те, що я повторюю з профілером в руці (а іноді і з кількома промахами по шляху, коли мені не вдалося прискорити роботу). І все-таки аспект дизайну інтерфейсу орієнтованого на дані дизайну - це те, що залишає мені можливість шукати все більш ефективне представлення даних.
Головне - спроектувати інтерфейси на більш грубому рівні, ніж ми зазвичай спокушаємося. Це також часто має такі побічні переваги, як зменшення накладних витрат динамічної диспетчеризації, пов’язаних з віртуальними функціями, виклики функцій вказівника, виклики диліб та неможливість викреслити. Основна ідея, щоб вийняти все це, - це дивитися на обробку масовим способом (коли це застосовується).
ball->do_something();
протиball_table.do_something(ball)
), якщо ви не хочете підробити цілісну сутність за допомогою псевдопоказника(&ball_table, index)
.