[...] (предоставляется в микросекундной среде) [...]
Микросекунды складываются, если мы повторяем миллионы и миллиарды вещей. Персональный сеанс 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 минут.
Оптимизация эффективности памяти часто может помочь в диапазоне от нескольких раз до порядков величин в однопоточном контексте и более в многопоточных контекстах (преимущества эффективного повторения памяти часто умножаются на несколько потоков в миксе).
О важности микрооптимизации
Я немного взволнован этой идеей, что микрооптимизация - пустая трата времени. Я согласен, что это хороший общий совет, но не все делают это неправильно, основываясь на догадках и суевериях, а не на измерениях. Сделано правильно, это не обязательно приведет к микро-воздействия. Если мы возьмем собственный процессор Intel Embree (ядро трассировки лучей) и протестируем только написанную ими простую скалярную BVH (а не пакет лучей, который экспоненциально сложнее превзойти), а затем попробуем побить производительность этой структуры данных, это может опыт смирения даже для ветерана, который десятилетиями использовал для профилирования и настройки кода. И все из-за примененной микрооптимизации. Их решение может обрабатывать более ста миллионов лучей в секунду, когда я видел промышленных специалистов, работающих в трассировке лучей, которые могут
Нет никакого способа взять прямую реализацию BVH с только алгоритмическим фокусом и получить более ста миллионов пересечений первичных лучей в секунду против любого оптимизирующего компилятора (даже собственного ICC Intel). Простое часто даже не получает миллион лучей в секунду. Требуются решения профессионального качества, чтобы часто получать даже несколько миллионов лучей в секунду. Требуется микрооптимизация уровня Intel, чтобы получить более ста миллионов лучей в секунду.
Алгоритмы
Я думаю, что микрооптимизация не важна, если производительность не важна на уровне минут или секунд, например, часов или минут. Если мы возьмем ужасающий алгоритм, такой как пузырьковая сортировка, и используем его в качестве примера для массового ввода, а затем сравним его даже с базовой реализацией сортировки слиянием, то первый может занять месяцы для обработки, а последний - 12 минут, в результате квадратичной и линейной сложности.
Разница между месяцами и минутами, вероятно, заставит большинство людей, даже тех, кто не работает в критических по производительности полях, считать время выполнения неприемлемым, если оно требует от пользователей, ожидающих месяцы, чтобы получить результат.
Между тем, если мы сравним не микрооптимизированную, простую сортировку слиянием с быстрой сортировкой (которая вовсе не алгоритмически превосходит сортировку слиянием и предлагает только улучшения на микроуровне для эталонного местоположения), микрооптимизированная быстрая сортировка может закончиться в 15 секунд вместо 12 минут. Заставить пользователей ждать 12 минут может быть вполне приемлемо (время перерыва на кофе).
Я думаю, что эта разница, вероятно, незначительна для большинства людей, скажем, между 12 минутами и 15 секундами, и именно поэтому микрооптимизацию часто считают бесполезной, поскольку зачастую она похожа только на разницу между минутами и секундами, а не минутами и месяцами. Другая причина, по которой я считаю ее бесполезной, заключается в том, что ее часто применяют к областям, которые не имеют значения: небольшая область, которая даже не является зацикленной и критической, что приводит к некоторой сомнительной разнице в 1% (которая вполне может быть просто шумом). Но для людей, которые заботятся об этих типах различий во времени и желают измерить и сделать это правильно, я думаю, что стоит обратить внимание, по крайней мере, на основные понятия иерархии памяти (особенно на верхние уровни, относящиеся к сбоям страниц и пропаданиям кэша) ,
Java оставляет много места для хорошей микрооптимизации
Фу, извините - с такими напыщенными словами:
Мешает ли «магия» JVM влиянию программиста на микрооптимизации в Java?
Немного, но не так много, как думают люди, если вы все сделаете правильно. Например, если вы выполняете обработку изображений в собственном коде с рукописным SIMD, многопоточностью и оптимизацией памяти (шаблоны доступа и, возможно, даже представление в зависимости от алгоритма обработки изображений), легко сократить сотни миллионов пикселей в секунду за 32- бит RGBA пикселей (8-битные цветные каналы), а иногда даже миллиарды в секунду.
Невозможно приблизиться к Java, если вы скажете, что создали 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
для каждого отдельного UDT).
В C ++ немного проще контролировать смежность разметки памяти между неоднородными полями (например, чередование чисел с плавающей точкой и целых в один массив через структуру / класс), поскольку пространственная локальность часто теряется (или, по крайней мере, теряется контроль) в Java при выделении объектов через GC.
... но часто решения с самой высокой производительностью часто в любом случае разделяют их и используют шаблон доступа SoA для непрерывных массивов простых старых данных. Таким образом, для областей, где требуется максимальная производительность, стратегии оптимизации размещения памяти между Java и C ++ часто одинаковы, и вам часто придется разрушать эти крошечные объектно-ориентированные интерфейсы в пользу интерфейсов в стиле коллекций, которые могут делать такие вещи, как hot / холодное разделение полей, повторы SoA и т. д. Неоднородные повторы AoSoA кажутся невозможными в Java (если только вы не использовали необработанный массив байтов или что-то в этом роде), но это для редких случаев, когда обашаблоны последовательного и произвольного доступа должны быть быстрыми, одновременно имея смесь типов полей для горячих полей. Для меня большая часть различий в стратегии оптимизации (на общем уровне) между этими двумя является спорным, если вы стремитесь к максимальной производительности.
Различия могут немного отличаться, если вы просто стремитесь к «хорошей» производительности - невозможность сделать что-либо с небольшими объектами, такими как Integer
vs., int
может быть немного больше PITA, особенно в том, как он взаимодействует с генериками. , Это немного сложнее просто построить один родовую структуру данных в качестве центральной цели оптимизации в Java , которая работает для int
, float
и т.д., избегая при этом эти большие и дорогие UDT, но часто наиболее критичной область потребует ручной прокатки своих собственных структур данных в любом случае настроен для очень конкретной цели, поэтому раздражает только код, который стремится к хорошей производительности, но не к максимальной производительности.
Объект накладных расходов
Обратите внимание, что издержки Java-объекта (метаданные и потеря пространственной локальности и временная потеря временной локальности после начального цикла GC) часто велики для действительно небольших вещей (например, int
против Integer
), которые хранятся миллионами в некоторой структуре данных, которая в основном смежные и доступны в очень узких петлях. Похоже, что этот предмет очень чувствителен, поэтому я должен пояснить, что вам не нужно беспокоиться об объектных накладных расходах для больших объектов, таких как изображения, просто очень незначительные объекты, такие как один пиксель.
Если кто-то сомневается в этой части, я бы предложил сделать сравнение между суммированием миллиона случайных ints
и миллионов случайных чисел Integers
и делать это повторно ( Integers
перестановка в памяти после начального цикла GC).
Окончательный трюк: дизайн интерфейса, который оставляет место для оптимизации
Итак, лучший трюк с Java, как я вижу, если вы имеете дело с местом, которое обрабатывает большую нагрузку на небольшие объекты (например Pixel
, a, 4-вектор, матрица 4x4, a Particle
, возможно, даже Account
если оно имеет только несколько маленьких поля), чтобы избежать использования объектов для этих маленьких вещей и использовать массивы (возможно, соединенные вместе) простых старых данных. Объекты становятся интерфейсами сбора , как Image
, ParticleSystem
, Accounts
, коллекция матриц или векторов и т.д. Отдельных из них можно получить по индексу, например , это также один из конечных трюков дизайна в C и C ++, поскольку даже без этого основных накладных объекта и Разобщенная память, моделирование интерфейса на уровне отдельной частицы мешает наиболее эффективным решениям.