Чи вважаєте ви, що існує компроміс між написанням «приємного» об'єктно-орієнтованого коду та написанням дуже швидкого коду з низькою затримкою? Наприклад, уникаючи віртуальних функцій у C ++ / накладних поліморфізмів тощо. - переписувати код, який виглядає неприємно, але дуже швидко тощо?
Я працюю в галузі, яка трохи більше орієнтована на пропускну здатність, ніж затримка, але це дуже критично для продуктивності, і я б сказав "сорта" .
Однак проблема полягає в тому, що так багато людей сприймають свої уявлення про продуктивність абсолютно неправильно. Новачки часто розуміють все не так, і вся їхня концептуальна модель "обчислювальної вартості" потребує переробки, лише алгоритмічна складність стосується єдиного, що вони можуть отримати правильно. Посередники неправильно дістають багато речей. Експерти помиляються на деякі речі.
Вимірювання за допомогою точних інструментів, які можуть надати такі показники, як пропуски кешу та непередбачувані галузеві функції, - це те, що забезпечує перевірку всіх людей будь-якого рівня знань у цій галузі.
Вимірювання - це також те, що вказує на те , що не оптимізувати . Експерти часто витрачають менше часу на оптимізацію, ніж у початківців, оскільки вони оптимізують справжні розмірені гарячі точки та не намагаються оптимізувати дикі колоти у темряві, ґрунтуючись на переслідуваннях щодо того, що може бути повільним (що, в крайній формі, може спокусити одного мікро-оптимізувати просто про кожен інший рядок у кодовій базі).
Проектування для продуктивності
З огляду на це, ключ до проектування для продуктивності походить від частини дизайну , як і в дизайні інтерфейсів. Однією з проблем недосвідченості є те, що існує тенденція до раннього переходу на абсолютні показники реалізації, як вартість виклику непрямої функції в якомусь узагальненому контексті, як би вартість (що краще зрозуміти в безпосередньому розумінні з точки зору оптимізатора). з точки зору, а не з розгалуженою точкою зору) є причиною уникати її у всій базі коду.
Витрати відносні . Хоча вартість виклику непрямої функції існує, наприклад, всі витрати відносні. Якщо ви одноразово оплачуєте цю вартість, щоб зателефонувати на функцію, яка перетворюється на мільйони елементів, турбуватися про цю вартість - це як витратити години на торг за копійки на придбання продукту в мільярд доларів, лише зробити висновок, щоб не купувати цей товар, оскільки він була одна копійка занадто дорога.
Грубіший дизайн інтерфейсу
Інтерфейс дизайн аспект продуктивності часто прагне раніше , щоб підштовхнути ці витрати до рівня крупнозернистого. Замість того, щоб платити витрати на абстракцію виконання однієї частинки, наприклад, ми можемо підштовхнути ці витрати до рівня системи / випромінювача частинок, ефективно перетворюючи частинку на деталі реалізації та / або просто необроблені дані цього колекції частинок.
Таким чином, об'єктно-орієнтований дизайн не повинен бути несумісним з проектуванням для продуктивності (будь то затримка чи пропускна здатність), але можуть бути спокуси мовою, яка орієнтується на нього, щоб моделювати все більш маленькі зернисті об'єкти, і там останній оптимізатор не може допомогу. Він не може робити такі речі, як об'єднання класу, що представляє єдину точку, таким чином, що дає ефективне представлення SoA для моделей доступу до пам'яті програмного забезпечення. Колекція точок з дизайном інтерфейсу, змодельованим на рівні грубості, пропонує таку можливість і дозволяє переорієнтуватися на все більш оптимальні рішення за потребою. Така конструкція розрахована на об'ємну пам'ять *.
* Зауважте, що тут зосереджено увагу на пам’яті, а не на даних , оскільки робота в критичних для продуктивного простору довгий час буде, як правило, змінювати ваше уявлення про типи даних та структури даних та бачити, як вони підключаються до пам'яті. Бінарне дерево пошуку вже не стосується лише логарифмічної складності в таких випадках, як можливо, розрізнені та непривабливі до кешу шматки пам’яті для деревних вузлів, якщо не допоможе фіксований розподільник. Погляд не відкидає алгоритмічну складність, але він не бачить його більше незалежно від макетів пам'яті. Крім того, починає розглядатися ітерацій роботи як більше про ітерації доступу до пам'яті. *
Багато критично важливих для продуктивності конструкцій насправді можуть бути дуже сумісними з поняттям дизайнів інтерфейсів високого рівня, які люди легко зрозуміти та використовувати. Різниця полягає в тому, що "високий рівень" в цьому контексті полягав би в об'ємній агрегації пам'яті, інтерфейсі, змодельованому для потенційно великих колекцій даних, і з реалізацією під кришкою, яка може бути досить низьким рівнем. Візуальна аналогія може бути автомобілем, який дійсно комфортний і простий у керуванні та керуванні, і дуже безпечний, коли рухається зі швидкістю звуку, але якщо попхнути капот, всередині мало маленьких вогняних демонів.
Завдяки більш грубій конструкції, як правило, пропонується більш простий спосіб забезпечити ефективніші схеми блокування та використовувати паралелізм у коді (багатопоточність - це вичерпний предмет, який я начебто пропускаю тут).
Басейн пам'яті
Найважливішим аспектом програмування з низькою затримкою, ймовірно, буде дуже чіткий контроль над пам'яттю для поліпшення місцевості відліку, а також просто загальної швидкості розподілу і розміщення пам'яті. Спеціальна пам'ять для об'єднання алокаторів насправді відповідає тому ж дизайнерському мисленню, яке ми описали. Він розроблений для навалу ; він розроблений на грубому рівні. Він попередньо розміщує пам'ять у великих блоках і об'єднує вже виділену пам'ять невеликими шматками.
Ідея точно така сама - підштовхувати дорогі речі (розподіляти шматок пам’яті проти розподільника загального призначення, наприклад) на більш грубий і грубіший рівень. Пул пам'яті призначений для масової роботи з пам'яттю .
Тип Системна роздільна пам'ять
Одна з труднощів із деталізованим об'єктно-орієнтованим дизайном на будь-якій мові полягає в тому, що він часто хоче представити багато типових, визначених користувачем типів і структур даних. Потім ці типи можуть бути виділені невеликими підлітковими фрагментами, якщо вони динамічно розподіляються.
Поширений приклад на C ++ - це випадки, коли потрібен поліморфізм, коли природним спокусою є виділення кожного примірника підкласу проти розподільника пам'яті загального призначення.
Це закінчується розбиттям можливих суміжних макетів пам'яті на невеликі іти-бітні біти та шматки, розкидані по діапазону адресації, що перетворює на більше помилок сторінки та пропусків кешу.
Поля, які вимагають детермінованої реакції з найменшою затримкою, без заїкань, ймовірно, це одне місце, де гарячі точки не завжди зводяться до єдиного вузького місця, де крихітні неефективність насправді справді може «накопичуватися» (щось багато хто собі уявляє трапляється неправильно з профілером, щоб перевірити їх, але в полях із затримкою насправді можуть бути рідкісні випадки, коли накопичуються крихітні неефективність). І дуже багато найпоширеніших причин такого скупчення можуть бути такими: надмірне виділення підліткових шматочків пам’яті всюди.
У таких мовах, як Java, може бути корисним використовувати більше масивів простих старих типів даних, коли це можливо для вузьких місць (областей, оброблених у вузьких петлях), таких як масив int
(але все ще знаходиться за об'ємним інтерфейсом високого рівня) замість, скажімо , об'єктів, ArrayList
визначених користувачем Integer
. Це дозволяє уникнути поділу пам'яті, яка зазвичай супроводжує останню. У C ++ нам не доведеться так сильно погіршувати структуру, якщо наші схеми розподілу пам’яті ефективні, оскільки визначені користувачем типи можуть постійно розподілятися там і навіть у контексті загального контейнера.
З'єднання пам'яті разом
Тут можна вирішити пошук спеціального розподільника для однорідних типів даних і, можливо, навіть для однорідних типів даних. Коли крихітні типи даних і структури даних згладжуються до бітів і байтів у пам'яті, вони набувають однорідного характеру (хоча з деякими різними вимогами до вирівнювання). Коли ми не дивимось на них із орієнтації на пам’ять, система типів мов програмування "хоче" розділити / розділити потенційно-суміжні області пам'яті на маленькі підліткові розсипані шматки.
Стек використовує цей фокус, орієнтований на пам'ять, щоб уникнути цього і потенційно зберігати будь-яку можливу змішану комбінацію визначених користувачем типів усередині нього. Використання стека більше - це відмінна ідея, коли можливо, оскільки верхня частина майже завжди сидить у кеш-лінії, але ми також можемо розробити розподільники пам'яті, які імітують деякі з цих характеристик без шаблону LIFO, перетворюючи пам'ять для різних типів даних у суміжні шматки навіть для більш складних моделей розподілу пам'яті та делокацій.
Сучасна апаратура розроблена таким чином, щоб вона була на своєму піку при обробці суміжних блоків пам'яті (багаторазово звертаючись до тієї ж лінії кешу, тієї ж сторінки, наприклад). Ключове слово є суміжність, оскільки це корисно лише за наявності оточуючих цікавих даних. Тому багато головного (але ще й складного) для продуктивності полягає в тому, щоб знову з’єднати відокремлені шматки пам’яті в суміжні блоки, до яких звертається в повному обсязі (усі навколишні дані є актуальними) перед виселенням. Система насиченого типу особливо визначених користувачем типів в мовах програмування може бути найбільшою перешкодою тут, але ми завжди можемо охопити і вирішити проблему за допомогою спеціального дизайнера та / або об'ємних конструкцій, коли це доречно.
Некрасивий
«Некрасиво» важко сказати. Це суб'єктивна метрика, і хтось, хто працює в дуже критичному для продуктивності полі, почне змінювати своє уявлення про "красу" на такий, який набагато більш орієнтований на дані і зосереджується на інтерфейсах, які обробляють речі масово.
Небезпечний
"Небезпечно" може бути простіше. Загалом, продуктивність прагне досягти коду нижчого рівня. Наприклад, реалізація розподільника пам’яті неможлива без досягнення типів даних та роботи на небезпечному рівні необроблених бітів та байтів. Як результат, це може допомогти збільшити увагу на ретельній процедурі тестування в цих критично важливих підсистемах, масштабуючи ретельність тестування з використанням рівня оптимізації, що застосовується.
Краса
Проте все це буде на рівні деталізації впровадження. І в масштабному, і в критичному відношенні режимі мислення "ветеран" "краса" має тенденцію переходити до дизайну інтерфейсів, а не до деталей реалізації. Це стає експоненціально вищим пріоритетом шукати "красиві", корисні, безпечні та ефективні інтерфейси, а не реалізацію через з'єднання та каскадні обриви, які можуть статися на тлі зміни дизайну інтерфейсу. Виконання можливо замінити будь-коли. Ми, як правило, відкладаємо до ефективності, як це вказують вимірювання. Головне в дизайні інтерфейсу - це моделювання на досить грубому рівні, щоб залишити місце для таких ітерацій, не порушуючи всю систему.
Насправді я хотів би припустити, що зосередженість ветерана на критично важливих робочих ситуаціях, як правило, зосереджує переважну увагу на безпеці, тестуванні, ремонтопридатності, а саме на учня SE в цілому, оскільки великомасштабна база коду, яка має ряд результатів -критичним підсистемам (системи частинок, алгоритми обробки зображень, обробка відео, звуковий зворотний зв'язок, променеві трекери, сітчасті двигуни тощо) потрібно буде приділяти пильну увагу інженерії програмного забезпечення, щоб уникнути утоплення в кошмарі технічного обслуговування. Не випадково часто найдивовижніші продукти можуть мати найменшу кількість помилок.
TL; DR
У будь-якому разі, це моя позиція щодо цієї теми, починаючи від пріоритетів у справді критичних сферах діяльності, що може зменшити затримку і призвести до накопичення крихітних неефективностей, і що насправді являє собою "красу" (якщо дивитися на речі найбільш продуктивно).