[...] (надано, у мікросекундному середовищі) [...]
Мікросекунди додаються, якщо ми перебираємо мільйони на мільярди речей. Особистий сеанс vtune / мікро-оптимізації з C ++ (без алгоритмічних удосконалень):
T-Rex (12.3 million facets):
Initial Time: 32.2372797 seconds
Multithreading: 7.4896073 seconds
4.9201039 seconds
4.6946372 seconds
3.261677 seconds
2.6988536 seconds
SIMD: 1.7831 seconds
4-valence patch optimization: 1.25007 seconds
0.978046 seconds
0.970057 seconds
0.911041 seconds
Все, окрім "багатопотокової", "SIMD" (написаної від руки до компілятора), та оптимізації 4-валентного патча були оптимізаціями пам'яті на рівні мікрорівні. Також оригінальний код, починаючи з початкового часу 32 секунди, вже був досить оптимізований (теоретично оптимальна алгоритмічна складність), і це нещодавній сеанс. Оригінальна версія задовго до цієї останньої сесії потребувала 5 хвилин на обробку.
Оптимізація ефективності пам’яті може допомогти часто в будь-якому місці від декількох разів до порядків величин в однопотоковому контексті та більше в багатопотокових контекстах (переваги ефективної повторної пам’яті часто множиться з кількох потоків у суміші).
Про важливість мікрооптимізації
Мене трохи засмучує думка, що мікрооптимізація - це марна трата часу. Я погоджуюсь, що це гарна загальна порада, але не всі роблять це неправильно на основі переслідувань і забобонів, а не замірів. Зроблено правильно, це не обов'язково дає мікро вплив. Якщо ми візьмемо власну Embree (ядро Raytracing) від Intel і перевіримо лише написаний ними простий скалярний BVH (а не пакет променів, який важко перемогти), а потім спробуємо перемогти продуктивність цієї структури даних, це може бути найбільше досвід буття навіть для ветерана, який десятиліттями звик до профілювання та налаштування коду. І все це завдяки застосованим мікрооптимізаціям. Їх рішення може обробляти понад сто мільйонів променів в секунду, коли я бачив промислових спеціалістів, які працюють в режимі проміння, хто може "
Немає можливості здійснити просту реалізацію BVH лише з алгоритмічним фокусом і отримати понад сто мільйонів первинних перетинів променів в секунду проти будь-якого оптимізуючого компілятора (навіть власного ICC IC). Безпосередньо один не отримує навіть мільйона променів в секунду. Потрібно професійно якісні рішення, щоб часто навіть отримати кілька мільйонів променів в секунду. Потрібна мікрооптимізація на рівні Intel, щоб отримати понад сто мільйонів променів в секунду.
Алгоритми
Я думаю, що мікрооптимізація не важлива, доки продуктивність не важлива на рівні хвилин до секунд, наприклад, годин або хвилин. Якщо ми візьмемо жахливий алгоритм на зразок сортування бульбашок і використаємо його за масовим введенням як приклад, а потім порівняємо його навіть із базовою реалізацією сортування злиття, для першої обробки може знадобитися кілька місяців, а останнього, можливо, 12 хвилин. квадратичної проти лінійно-складної складності.
Різниця між місяцями та хвилинами, ймовірно, змусить більшість людей, навіть тих, хто не працює в критичних для продуктивності полів, вважати час виконання неприйнятним, якщо для отримання результату потрібні користувачі, які чекають місяцями.
Тим часом, якщо порівняти немікрооптимізований, прямого сортування злиття з швидким сортуванням (що зовсім не алгоритмічно перевершує сортування злиття, а пропонує лише поліпшення на мікрорівні для місцевості відліку), мікрооптимізований кваксорт може закінчитися 15 секунд на відміну від 12 хвилин. Змусити користувачів чекати 12 хвилин може бути цілком прийнятним (час перерви на каву).
Я думаю, що ця різниця, мабуть, незначна для більшості людей між, скажімо, 12 хвилинами та 15 секундами, і тому мікрооптимізація часто вважається марною, оскільки часто подобається лише різниця між хвилинами та секундами, а не хвилинами та місяцями. Інша причина, яку я вважаю марною - це те, що вона часто застосовується до неважливих областей: невелика площа, яка не є навіть петельною і критичною, що дає певну різницю в 1% (що може бути просто шумом). Але людям, які піклуються про такі типи різниць у часі і готові вимірювати та робити це правильно, я думаю, що варто звернути увагу принаймні на основні поняття ієрархії пам’яті (зокрема верхні рівні, що стосуються помилок сторінки та пропуску кешу) .
Java залишає багато місця для хорошої мікрооптимізації
Фу, вибачте - з таким виглядом гнів:
Чи заважає "магія" JVM впливати програмісту на мікрооптимізацію на Java?
Трохи, але не настільки, як люди можуть подумати, якщо ви зробите це правильно. Наприклад, якщо ви займаєтесь обробкою зображень, в натурному коді з рукописними оптимізаціями SIMD, багатопотоковості та пам’яті (шаблони доступу та, можливо, навіть представлення залежно від алгоритму обробки зображень), легко розчавити сотні мільйонів пікселів в секунду за 32- бітові пікселі RGBA (8-бітові кольорові канали), а іноді навіть мільярди в секунду.
Наблизитись до Яви неможливо, якщо, скажімо, зробив Pixel
об’єкт (один лише надув би розмір пікселя від 4 байтів до 16 на 64-розрядному).
Але ви, можливо, зможете наблизитись набагато більше, якби уникнути Pixel
об'єкта, використали масив байтів та змоделювали Image
об’єкт. Java все ще досить компетентна, якщо ви починаєте використовувати масиви простих старих даних. Я раніше пробував подібні речі на Java і був дуже вражений, якщо ви не створюєте кучу маленьких підліткових об'єктів скрізь, що в 4 рази більше, ніж зазвичай (наприклад: використовувати int
замість Integer
), і починати моделювати об'ємні інтерфейси, як Image
інтерфейс, а не Pixel
інтерфейс. Я б навіть зважився сказати, що Java може конкурувати з програмою C ++, якщо ви перебираєте звичайні старі дані, а не об'єкти (величезні масиви float
, наприклад, ні Float
).
Можливо, навіть важливіше, ніж розміри пам'яті, - це те, що масив int
гарантій суцільного подання. Масив Integer
не робить. Близькість часто є важливою для місцевості відліку, оскільки це означає, що декілька елементів (наприклад: 16 ints
) можуть усі вписатись в одну лінію кешу і, можливо, отримати доступ до них разом перед тим, як виселити ефективні схеми доступу до пам'яті. Між тим, одне Integer
може бути розташоване десь у пам’яті, коли оточуюча пам’ять не має значення, тільки щоб ця область пам’яті була завантажена в кеш-рядок лише для використання одного цілого числа до виселення на відміну від 16 цілих чисел. Навіть якби нам надзвичайно пощастило та оточилиIntegers
у пам’яті все було поряд, ми можемо вмістити лише 4 у кеш-рядок, до якого можна отримати доступ до виселення, оскільки це в Integer
4 рази більше, і це в найкращому випадку.
І є багато мікрооптимізацій, оскільки ми об’єднані в одній архітектурі / ієрархії пам'яті. Шаблони доступу до пам’яті не залежать від того, якою мовою ви користуєтесь, такі поняття, як нав'язування циклу чи блокування циклу, зазвичай, можна застосовувати набагато частіше на C або C ++, але вони так само корисні Java.
Я нещодавно читав на C ++, іноді впорядкування членів даних може забезпечити оптимізацію [...]
Порядок членів даних, як правило, не має значення в Java, але в основному це добре. У C і C ++ збереження порядку членів даних часто важливо з причин ABI, тому компілятори з цим не псуються. Людські розробники, які працюють там, повинні бути обережними, щоб робити такі дії, як упорядкувати своїх членів даних у порядку зменшення (найбільший до найменшого), щоб уникнути втрати пам'яті на прокладку. З Java, очевидно, JIT може впорядкувати членів для вас на ходу, щоб забезпечити правильне вирівнювання, мінімізуючи підкладку, тому за умови, що це так, він автоматизує щось, що середні програмісти C і C ++ часто можуть погано робити і в кінцевому підсумку витрачають пам'ять таким чином ( що не просто витрачає пам'ять, але часто витрачає швидкість, збільшуючи крок між структурами AoS без потреби і спричиняючи більше пропусків кешу). Це ' дуже робототехнічна річ, щоб переставляти поля, щоб мінімізувати забивання, тому в ідеалі люди не займаються цим. Єдиний час, коли розташування полів може мати значення таким чином, що вимагає від людини знання оптимального розташування, якщо об'єкт більший за 64 байти, і ми організовуємо поля на основі шаблону доступу (а не оптимального набивання) - у такому випадку це може бути більш людським завданням (вимагає розуміння критичних шляхів, частина яких - це інформація, яку компілятор неможливо передбачити, не знаючи, що робитимуть користувачі із цим програмним забезпеченням).
Якщо ні, чи могли б люди навести приклади, які трюки можна використовувати на Java (окрім простих прапорів компілятора).
Найбільша різниця для мене з точки зору оптимізації ментальності між Java та C ++ полягає в тому, що C ++ може дозволити вам використовувати об'єкти трохи (підлітковий) трохи більше, ніж Java у критичному для продуктивного сценарію. Наприклад, C ++ може перенести ціле число до класу без накладних витрат (орієнтир у всьому місці). Java має мати метадані вказівника + вирівнювання накладних накладних витрат на об'єкт, тому Boolean
більше boolean
(але в обмін, що забезпечує однакові переваги відображення та можливість змінити будь-яку функцію, не позначену як final
для кожної окремої УДТ).
У C ++ трохи легше керувати безперервністю макетів пам’яті через неоднорідні поля (наприклад: перемежування плаває і вводиться в один масив через структуру / клас), оскільки просторова локальність часто втрачається (або принаймні втрачається контроль) в Java при розподілі об'єктів через GC.
... але найчастіше рішення з найвищою ефективністю часто все-таки розділять їх і використовуватимуть шаблон доступу SoA над суміжними масивами простих старих даних. Тож для областей, які потребують пікової продуктивності, стратегії оптимізації компонування пам’яті між Java та C ++ часто однакові, і часто вам доведеться знести ці маленькі об’єктно-орієнтовані інтерфейси на користь інтерфейсів стилю колекції, які можуть робити такі речі, як гаряче / розщеплення холодного поля, повторення SoA і т. д. Неоднорідні повтори AoSoA здаються неможливими на Java (якщо ви просто не використовували необроблений масив байтів чи щось подібне), але це для рідкісних випадків, коли обидвашаблони послідовного та випадкового доступу повинні бути швидкими, одночасно маючи суміш типів полів для гарячих полів. Для мене основна частина різниці в стратегії оптимізації (на загальному рівні) між цими двома є суперечливою, якщо ви досягаєте пікових показників.
Відмінності трохи розрізняються більше , якщо ви просто дістаючи «хорошу» продуктивність - не в змозі зробити так само з невеликими об'єктами , як Integer
VS. int
може бути трохи більше PITA, особливо з тим , як він взаємодіє з узагальненнями . Трохи складніше просто створити одну загальну структуру даних як центральну ціль оптимізації в Java, яка працює для int
, float
тощо., Уникаючи тих великих і дорогих UDT, але часто найважливіші області роботи вимагають ручної прокатки власних структур даних. налаштований на дуже конкретну мету, так що це тільки дратує код, який прагне до хорошої продуктивності, але не пікової продуктивності.
Об'єкт накладні
Зауважте, що накладні об'єкти Java (метадані та втрата просторової локальності та тимчасова втрата тимчасової локальності після початкового циклу GC) часто є великими для речей, які насправді є невеликими (наприклад, int
порівняно з Integer
), які зберігаються мільйонами в деякій структурі даних, значною мірою суміжні та мають доступ у дуже тугих петлях. Здається, що з цього приводу є багато чутливості, тому я повинен уточнити, що ви не хочете турбуватися про накладні об'єкти для великих об’єктів, таких як зображення, просто насправді мізерні об'єкти, такі як один піксель.
Якщо хтось відчуває сумніви щодо цієї частини, я б запропонував зробити орієнтир між підсумовуванням мільйона випадкових ints
порівняно з мільйоном випадкових випадків Integers
і робити це повторно ( Integers
вольові зміни перейдуть в пам'ять після початкового циклу GC).
Ultimate Trick: Дизайн інтерфейсу, який залишає місце для оптимізації
Отже, найвищий фокус Java, як я бачу, якщо ви маєте справу з місцем, яке обробляє велике навантаження над дрібними предметами (наприклад: a Pixel
, 4-вектор, матриця 4x4, a Particle
, можливо, навіть Account
якщо у ньому є лише кілька малих полів) - це уникати використання об'єктів для цих маленьких речей та використання масивів (можливо, пов'язаних між собою) простих старих даних. Об'єкти стають інтерфейсами збору , як Image
, ParticleSystem
, Accounts
, колекція матриць або векторів і т.д. Окремих з них можна отримати за індексом, наприклад , це також один з кінцевих трюків дизайну в C і C ++, оскільки навіть без цього основних накладних об'єкта і роз'єднана пам'ять, моделювання інтерфейсу на рівні однієї частинки перешкоджає найбільш ефективним рішенням.