Але чи може цей OOP бути недоліком для програмного забезпечення на основі продуктивності, тобто як швидко виконується програма?
Часто так !!! АЛЕ ...
Іншими словами, чи може багато посилань між багатьма різними об'єктами або, використовуючи багато методів з багатьох класів, призвести до "важкої" реалізації?
Не обов'язково. Це залежить від мови / упорядника. Наприклад, оптимізуючий компілятор C ++, за умови, що ви не використовуєте віртуальні функції, часто прибиває ваш об'єкт до нуля. Ви можете робити такі речі, як написати обгортку над int
туди або обшитий розумний вказівник на звичайний старий вказівник, який виконує так само швидко, як безпосередньо використання цих простих старих типів даних.
В інших мовах, таких як Java, є деякий наклад на об'єкт (часто він досить малий у багатьох випадках, але астрономічний у деяких рідкісних випадках із дійсно маленькими об'єктами). Наприклад, Integer
він значно менш ефективний, ніж int
(займає 16 байт на відміну від 4 на 64-розрядному). Але це не просто кричущі відходи або щось подібне. В обмін на те, Java пропонує такі речі, як відображення кожного визначеного користувачем типу рівномірно, а також можливість змінити будь-яку функцію, не позначену як final
.
Але давайте візьмемо найкращий сценарій: оптимізуючий компілятор C ++, який може оптимізувати інтерфейси об'єктів до нуля накладних витрат. Навіть тоді OOP часто погіршує продуктивність і не дасть їй досягти піку. Це може звучати як повний парадокс: як це могло бути? Проблема полягає в:
Дизайн інтерфейсу та інкапсуляція
Проблема полягає в тому, що навіть коли компілятор може скоротити структуру об'єкта до нуля накладних витрат (що принаймні дуже часто справедливо для оптимізації компіляторів C ++), інкапсуляція та дизайн інтерфейсу (та накопичені залежності) дрібнозернистих об'єктів часто запобігають найоптимальніші подання даних для об'єктів, які призначені для агрегування мас (що часто трапляється для критичного продуктивного програмного забезпечення).
Візьмемо цей приклад:
class Particle
{
public:
...
private:
double birth; // 8 bytes
float x; // 4 bytes
float y; // 4 bytes
float z; // 4 bytes
/*padding*/ // 4 bytes of padding
};
Particle particles[1000000]; // 1mil particles (~24 megs)
Скажімо, наша схема доступу до пам’яті полягає в тому, щоб просто перебирати ці частинки послідовно і переміщати їх по кожному кадру повторно, відскакуючи їх за кути екрану, а потім надаючи результат.
Вже зараз ми можемо побачити яскраві 4-байтні накладки, необхідні для birth
правильного вирівнювання елемента, коли частинки безперервно агрегуються. Вже ~ 16,7% пам'яті витрачається на мертвий простір, який використовується для вирівнювання.
Це може здатися суперечливим, оскільки у нас сьогодні є гігабайти DRAM. Але навіть у самих звіриних машин у нас часто є лише 8 мегабайт, якщо мова йде про найповільніший і найбільший регіон кеш-процесора (L3). Чим менше ми можемо поміститися там, тим більше ми заплатимо за це за багаторазовий доступ до DRAM, і чим повільніше вийдемо. Раптом втрата 16,7% пам’яті вже не здається тривіальною угодою.
Ми можемо легко усунути цю накладну без будь-якого впливу на вирівнювання поля:
class Particle
{
public:
...
private:
float x; // 4 bytes
float y; // 4 bytes
float z; // 4 bytes
};
Particle particles[1000000]; // 1mil particles (~12 megs)
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
Тепер ми зменшили пам’ять з 24 мег до 20 мег. За допомогою послідовного шаблону доступу машина тепер споживає ці дані трохи швидше.
Але давайте розглянемо це birth
поле трохи уважніше. Скажімо, він фіксує час початку, коли частинка народжується (створюється). Уявіть, що поле доступне лише тоді, коли частинка вперше створена, і кожні 10 секунд, щоб побачити, чи повинна частинка відмирати та перероджуватися у випадковому місці на екрані. У цьому випадку birth
- це холодне поле. Це не доступно в критичних для циклу виконання циклів.
Як наслідок, фактично важливі дані щодо продуктивності - це не 20 мегабайт, а фактично суміжний блок на 12 Мбайт. Фактична гаряча пам’ять, до якої ми часто звертаємось, скоротилася до половини свого розміру! Очікуйте значних прискорень роботи над нашим оригінальним, 24-мегабайтним рішенням (не потрібно вимірювати - вже зроблено подібний матеріал тисячу разів, але почувайтеся вільно, якщо сумніваєтесь).
Все ж помічайте, що ми тут робили. Ми повністю розбили інкапсуляцію цього об’єкта частинок. Його стан тепер розділений між Particle
приватними полями типу та окремим паралельним масивом. І ось тут дещо перешкоджає гранульований об’єктно-орієнтований дизайн.
Ми не можемо виразити оптимальне представлення даних, якщо обмежитися дизайном інтерфейсу одного, дуже зернистого об'єкта, як-от одначаста частинка, один-піксель, навіть один 4-компонентний вектор, можливо, навіть один об'єкт "істота" в грі , і т. д. Швидкість гепарду буде витрачена даремно, якщо він стоїть на маленькому острові площею 2 кв. м, і саме це дуже гранульована об'єктно-орієнтована конструкція часто робить з точки зору продуктивності. Це обмежує подання даних на неоптимальний характер.
Щоб продовжити це, скажімо, що оскільки ми просто переміщуємо частинки навколо, ми можемо отримати доступ до їх полів x / y / z у трьох окремих петлях. У цьому випадку ми можемо скористатись SIMD-стилями SIMD-стилів з регістрами AVX, які можуть векторизувати 8 операцій SPFP паралельно. Але для цього ми повинні використовувати це представлення:
float particle_x[1000000]; // 1mil particle X positions (~4 megs)
float particle_y[1000000]; // 1mil particle Y positions (~4 megs)
float particle_z[1000000]; // 1mil particle Z positions (~4 megs)
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
Зараз ми літаємо за допомогою моделювання частинок, але подивіться, що сталося з нашим дизайном частинок. Він повністю зруйнований, і ми зараз дивимось на 4 паралельних масиви і жодного об'єкта для їх об'єднання не було б. Наш об'єктно-орієнтований Particle
дизайн пішов у сайдорару.
Це траплялося зі мною багато разів, працюючи в критичних для продуктивності сферах, де користувачі вимагають швидкості, тільки правильність - це одне, чого вони вимагають більше. Ці маленькі підліткові об'єктно-орієнтовані конструкції довелося знести, а каскадні поломки часто вимагали, щоб ми використовували стратегію повільної амортизації для швидшого проектування.
Рішення
Наведений вище сценарій представляє лише проблему із деталізованими об'єктно-орієнтованими конструкціями. У цих випадках нам часто доводиться зносити структуру, щоб висловити більш ефективні уявлення в результаті повторень SoA, розбиття гарячого / холодного поля, зменшення оббивки для послідовних шаблонів доступу (прокладка іноді корисна для продуктивності з випадковим доступом шаблонів у випадках AoS, але майже завжди є перешкодою для послідовних моделей доступу) тощо.
Але ми можемо прийняти остаточне подання, на якому ми опинилися і все ще моделюємо об'єктно-орієнтований інтерфейс:
// Represents a collection of particles.
class ParticleSystem
{
public:
...
private:
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
float particle_x[1000000]; // 1mil particle X positions (~4 megs)
float particle_y[1000000]; // 1mil particle Y positions (~4 megs)
float particle_z[1000000]; // 1mil particle Z positions (~4 megs)
};
Тепер нам добре. Ми можемо отримати всі об'єктно-орієнтовані смаколики, які нам подобаються. У гепарда є ціла країна, яку можна пробігти якомога швидше. Наші дизайни інтерфейсів більше не втягують нас у вузький куточок.
ParticleSystem
потенційно навіть може бути абстрактним і використовувати віртуальні функції. Зараз це суперечка, ми платимо за накладні витрати на рівні збору частинок, а не на рівні частинок . Накладні витрати - це 1/1000000, що було б інакше, якби ми моделювали об'єкти на рівні окремих частинок.
Тож це рішення у справжньо важливих для продуктивності областях, що справляються з великим навантаженням, і для всіх видів мов програмування (ця методика виграє на C, C ++, Python, Java, JavaScript, Lua, Swift тощо). І це не може бути легко позначено як "передчасна оптимізація", оскільки це стосується дизайну інтерфейсу та архітектури . Ми не можемо записати базу коду, що моделює одну частинку як об'єкт із завантаженою залежністю клієнта до aParticle's
публічний інтерфейс, а згодом передумайте. Я багато чого зробив, коли закликали оптимізувати застарілі бази даних, і це може призвести до того, що потрібно багато місяців ретельно переписати десятки тисяч рядків коду для використання об'ємного дизайну. Це в ідеалі впливає на те, як ми проектуємо речі наперед, за умови, що ми можемо передбачити велике навантаження.
Я продовжую повторювати цю відповідь у тій чи іншій формі у багатьох питаннях щодо продуктивності, особливо тих, що стосуються об'єктно-орієнтованого дизайну. Об'єктно-орієнтований дизайн все ще може бути сумісним із потребами у виробництві з найвищим попитом, але нам доведеться трохи змінити те, як ми про це думаємо. Ми повинні надати цьому гепарду деякий простір, щоб він працював так швидко, як це можливо, а це часто неможливо, якщо ми створимо маленькі маленькі предмети, які ледве зберігають будь-який стан.