Мешает ли «магия» JVM влиянию программиста на микрооптимизации в Java? Я недавно читал на C ++, иногда упорядочение членов данных может обеспечить оптимизацию (предоставляется в микросекундной среде), и я предположил, что руки программиста связаны, когда дело доходит до сжатия производительности из Java?
Я ценю, что приличный алгоритм обеспечивает больший выигрыш в скорости, но если у вас есть правильный алгоритм, сложнее ли настроить Java из-за управления JVM?
Если нет, могут ли люди привести примеры того, какие приемы вы можете использовать в Java (помимо простых флагов компилятора).
java
c++
performance
latency
user997112
источник
источник
Ответы:
Несомненно, на уровне микрооптимизации JVM будет делать некоторые вещи, которые вы будете иметь под небольшим контролем, особенно по сравнению с C и C ++.
С другой стороны, разнообразие поведений компилятора с C и C ++ особенно окажет гораздо большее негативное влияние на вашу способность выполнять микрооптимизацию любым неопределенно переносимым способом (даже между версиями компилятора).
Это зависит от того, какой проект вы настраиваете, какие среды вы нацеливаете и так далее. И, в конце концов, это не имеет большого значения, так как вы в любом случае получаете на несколько порядков лучшие результаты от алгоритмической / структуры данных / оптимизации дизайна программы.
источник
Микро-оптимизации почти никогда не стоят времени, и почти все простые выполняются автоматически компиляторами и средами выполнения.
Однако есть одна важная область оптимизации, где C ++ и Java принципиально отличаются, а именно массовый доступ к памяти. C ++ имеет ручное управление памятью, что означает, что вы можете оптимизировать структуру данных приложения и шаблоны доступа, чтобы в полной мере использовать кэши. Это довольно сложно, в некоторой степени относится к оборудованию, на котором вы работаете (поэтому прирост производительности может исчезнуть на другом оборудовании), но если все сделано правильно, это может привести к абсолютно захватывающей производительности. Конечно, вы платите за это с потенциалом для всех видов ужасных ошибок.
С таким языком сборки мусора, как Java, такого рода оптимизации невозможно выполнить в коде. Некоторые могут быть выполнены во время выполнения (автоматически или с помощью конфигурации, см. Ниже), а некоторые просто невозможны (цена, которую вы платите за защиту от ошибок управления памятью).
Флаги компилятора не имеют значения в Java, потому что компилятор Java практически не оптимизируется; время выполнения делает.
И действительно, среда выполнения Java имеет множество параметров, которые можно настроить, особенно в отношении сборщика мусора. В этих параметрах нет ничего «простого» - значения по умолчанию хороши для большинства приложений, а для повышения производительности необходимо точно понимать, что делают эти параметры и как работает ваше приложение.
источник
Микросекунды складываются, если мы повторяем миллионы и миллиарды вещей. Персональный сеанс vtune / микро-оптимизации из C ++ (без улучшений алгоритма):
Все, кроме «многопоточности», «SIMD» (написано от руки, чтобы превзойти компилятор), и оптимизация патча с 4 валентностями были оптимизацией памяти на микроуровне. Также оригинальный код, начиная с начального времени 32 секунд, уже был немного оптимизирован (теоретически оптимальная алгоритмическая сложность), и это недавняя сессия. Первоначальная версия задолго до этой недавней сессии заняла более 5 минут.
Оптимизация эффективности памяти часто может помочь в диапазоне от нескольких раз до порядков величин в однопоточном контексте и более в многопоточных контекстах (преимущества эффективного повторения памяти часто умножаются на несколько потоков в миксе).
О важности микрооптимизации
Я немного взволнован этой идеей, что микрооптимизация - пустая трата времени. Я согласен, что это хороший общий совет, но не все делают это неправильно, основываясь на догадках и суевериях, а не на измерениях. Сделано правильно, это не обязательно приведет к микро-воздействия. Если мы возьмем собственный процессор Intel Embree (ядро трассировки лучей) и протестируем только написанную ими простую скалярную BVH (а не пакет лучей, который экспоненциально сложнее превзойти), а затем попробуем побить производительность этой структуры данных, это может опыт смирения даже для ветерана, который десятилетиями использовал для профилирования и настройки кода. И все из-за примененной микрооптимизации. Их решение может обрабатывать более ста миллионов лучей в секунду, когда я видел промышленных специалистов, работающих в трассировке лучей, которые могут
Нет никакого способа взять прямую реализацию BVH с только алгоритмическим фокусом и получить более ста миллионов пересечений первичных лучей в секунду против любого оптимизирующего компилятора (даже собственного ICC Intel). Простое часто даже не получает миллион лучей в секунду. Требуются решения профессионального качества, чтобы часто получать даже несколько миллионов лучей в секунду. Требуется микрооптимизация уровня Intel, чтобы получить более ста миллионов лучей в секунду.
Алгоритмы
Я думаю, что микрооптимизация не важна, если производительность не важна на уровне минут или секунд, например, часов или минут. Если мы возьмем ужасающий алгоритм, такой как пузырьковая сортировка, и используем его в качестве примера для массового ввода, а затем сравним его даже с базовой реализацией сортировки слиянием, то первый может занять месяцы для обработки, а последний - 12 минут, в результате квадратичной и линейной сложности.
Разница между месяцами и минутами, вероятно, заставит большинство людей, даже тех, кто не работает в критических по производительности полях, считать время выполнения неприемлемым, если оно требует от пользователей, ожидающих месяцы, чтобы получить результат.
Между тем, если мы сравним не микрооптимизированную, простую сортировку слиянием с быстрой сортировкой (которая вовсе не алгоритмически превосходит сортировку слиянием и предлагает только улучшения на микроуровне для эталонного местоположения), микрооптимизированная быстрая сортировка может закончиться в 15 секунд вместо 12 минут. Заставить пользователей ждать 12 минут может быть вполне приемлемо (время перерыва на кофе).
Я думаю, что эта разница, вероятно, незначительна для большинства людей, скажем, между 12 минутами и 15 секундами, и именно поэтому микрооптимизацию часто считают бесполезной, поскольку зачастую она похожа только на разницу между минутами и секундами, а не минутами и месяцами. Другая причина, по которой я считаю ее бесполезной, заключается в том, что ее часто применяют к областям, которые не имеют значения: небольшая область, которая даже не является зацикленной и критической, что приводит к некоторой сомнительной разнице в 1% (которая вполне может быть просто шумом). Но для людей, которые заботятся об этих типах различий во времени и желают измерить и сделать это правильно, я думаю, что стоит обратить внимание, по крайней мере, на основные понятия иерархии памяти (особенно на верхние уровни, относящиеся к сбоям страниц и пропаданиям кэша) ,
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
не имеет. Смежность часто имеет важное значение для локальности ссылок, поскольку это означает, что несколько элементов (например, 16ints
) могут вмещаться в одну строку кэша и потенциально могут быть доступны вместе до выселения с помощью эффективных схем доступа к памяти. Между тем, один элементInteger
может находиться в памяти где-то в памяти, причем окружающая память не имеет значения, только для того, чтобы эта область памяти была загружена в строку кэша только для использования одного целого числа перед вытеснением, а не 16 целых чисел. Даже если нам повезло и окружающимIntegers
если в памяти все в порядке, мы можем поместить только 4 в строку кэша, к которой можно получить доступ до выселения, поскольку вInteger
4 раза больше, и это в лучшем случае.И там есть много микрооптимизаций, поскольку мы объединены единой архитектурой / иерархией памяти. Шаблоны доступа к памяти не имеют значения, какой бы язык вы ни использовали, такие понятия, как разбиение на блоки / блокировка цикла, обычно могут применяться гораздо чаще в C или C ++, но они также приносят пользу Java.
Порядок членов данных, как правило, не имеет значения в Java, но это в основном хорошая вещь. В C и C ++ сохранение порядка элементов данных часто важно по причинам ABI, поэтому компиляторы не вмешиваются в это. Люди-разработчики, работающие там, должны быть осторожны, чтобы упорядочить свои элементы данных в порядке убывания (от наибольшего к наименьшему), чтобы не тратить память на заполнение. В Java, очевидно, JIT может переупорядочивать элементы для вас на лету, чтобы обеспечить правильное выравнивание при минимизации заполнения, поэтому при условии, что это так, он автоматизирует что-то, что обычные программисты на C и C ++ часто могут делать плохо, и в итоге тратит память таким образом ( который не просто тратит впустую память, но часто тратит впустую скорость, бесполезно увеличивая шаг между структурами AoS и вызывая больше промахов кэша). Это' Это очень роботизированная вещь для перестановки полей, чтобы минимизировать заполнение, поэтому в идеале люди не имеют с этим дело. Единственный случай, когда расположение полей может иметь значение таким образом, что человеку необходимо знать оптимальное расположение, - это если объект больше 64 байт, и мы упорядочиваем поля на основе шаблона доступа (не оптимального заполнения) - в этом случае это может быть более человеческим делом (требует понимания критических путей, часть из которых - информация, которую компилятор не может предвидеть, не зная, что пользователи будут делать с программным обеспечением).
Самое большое различие для меня с точки зрения оптимизации менталитета между 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, aParticle
, возможно, дажеAccount
если оно имеет только несколько маленьких поля), чтобы избежать использования объектов для этих маленьких вещей и использовать массивы (возможно, соединенные вместе) простых старых данных. Объекты становятся интерфейсами сбора , какImage
,ParticleSystem
,Accounts
, коллекция матриц или векторов и т.д. Отдельных из них можно получить по индексу, например , это также один из конечных трюков дизайна в C и C ++, поскольку даже без этого основных накладных объекта и Разобщенная память, моделирование интерфейса на уровне отдельной частицы мешает наиболее эффективным решениям.источник
user204677
ушел. Такой отличный ответ.Существует средняя область между микрооптимизацией, с одной стороны, и хорошим выбором алгоритма, с другой.
Это область ускорений с постоянным коэффициентом, и она может давать порядки величин.
Это делается путем отсечения целых долей времени выполнения, таких как сначала 30%, затем 20% того, что осталось, затем 50% этого и так далее в течение нескольких итераций, пока почти ничего не останется.
Вы не видите этого в маленьких программах в демо-стиле. Вы видите это в больших серьезных программах с большим количеством структур данных классов, где стек вызовов обычно имеет многоуровневую структуру. Хороший способ найти возможности ускорения - это изучить случайные выборки состояния программы.
Обычно ускорения состоят из таких вещей, как:
сведение к минимуму вызовов
new
путем объединения и повторного использования старых объектов,признавая, что делается что-то вроде ради общности, а не на самом деле необходимо,
пересмотр структуры данных с использованием различных классов сбора, которые имеют одинаковое поведение big-O, но используют преимущества фактически используемых шаблонов доступа,
сохранение данных, полученных вызовами функций, вместо повторного вызова функции (это естественная и забавная тенденция программистов предполагать, что функции с более короткими именами выполняются быстрее.)
допуская определенную степень несоответствия между избыточными структурами данных, вместо того, чтобы пытаться поддерживать их в полном соответствии с событиями уведомлений,
и т. д.
Но, конечно, ни одна из этих вещей не должна быть сделана без проблем с отбором проб.
источник
Java (насколько я знаю) не дает вам контроля над расположением переменных в памяти, поэтому вам труднее избежать таких вещей, как ложное разделение и выравнивание переменных (вы можете добавить класс с несколькими неиспользуемыми членами). Еще одна вещь, которую я не думаю, что вы можете воспользоваться такими инструкциями, как
mmpause
, но эти вещи зависят от процессора, и поэтому, если вы считаете, что вам это нужно, Java может не быть языком для использования.Существует класс Unsafe, который дает вам гибкость C / C ++, но также и с опасностью C / C ++.
Это может помочь вам взглянуть на ассемблерный код, который JVM генерирует для вашего кода.
Чтобы прочитать о Java-приложении, которое рассматривает такие подробности, смотрите код Disruptor, выпущенный LMAX
источник
На этот вопрос очень сложно ответить, потому что это зависит от языковых реализаций.
В общем, сейчас очень мало места для такой «микрооптимизации». Основная причина в том, что компиляторы используют преимущества такой оптимизации во время компиляции. Например, в ситуациях, когда их семантика идентична, нет никакой разницы в производительности между операторами до увеличения и после увеличения. Другим примером может служить, например, такой цикл, в
for(int i=0; i<vec.size(); i++)
котором можно утверждать, что вместо вызоваsize()
Функция-член во время каждой итерации было бы лучше получить размер вектора перед циклом, а затем сравнить с этой единственной переменной и, таким образом, избежать вызова функции за одну итерацию. Однако в некоторых случаях компилятор обнаруживает этот глупый случай и кэширует результат. Однако это возможно только в том случае, если функция не имеет побочных эффектов, и компилятор может быть уверен, что размер вектора остается постоянным в течение цикла, поэтому он применяется только к довольно тривиальным случаям.источник
const
методы для этого вектора, я уверен, что многие оптимизирующие компиляторы это поймут.Помимо улучшения алгоритмов, обязательно рассмотрим иерархию памяти и как процессор использует его. Снижение задержек при доступе к памяти дает большие преимущества, когда вы понимаете, как рассматриваемый язык распределяет память по типам данных и объектам.
Пример Java для доступа к массиву 1000x1000 дюймов
Рассмотрим приведенный ниже пример кода - он обращается к той же области памяти (массив целых 1000x1000), но в другом порядке. На моем Mac mini (Core i7, 2,7 ГГц) вывод выглядит следующим образом, показывая, что обход массива по строкам более чем удваивает производительность (в среднем более 100 раундов каждый).
Это связано с тем, что массив хранится таким образом, что последовательные столбцы (т. Е. Значения int) размещаются рядом в памяти, а последовательные строки - нет. Чтобы процессор действительно использовал данные, они должны быть переданы в свои кэши. Передача памяти осуществляется блоком байтов, называемым строкой кэша - загрузка строки кэша непосредственно из памяти приводит к задержкам и, таким образом, снижает производительность программы.
Для Core i7 (песчаный мост) строка кэша содержит 64 байта, таким образом, каждый доступ к памяти извлекает 64 байта. Поскольку первый тест обращается к памяти в предсказуемой последовательности, процессор будет предварительно извлекать данные до того, как они будут фактически использованы программой. В целом, это приводит к меньшей задержке при обращении к памяти и, таким образом, повышает производительность.
Код образца:
источник
JVM может и часто создает помехи, и JIT-компилятор может значительно меняться между версиями. Некоторые микрооптимизации невозможны в Java из-за языковых ограничений, таких как дружественность к гиперпоточности или коллекция SIMD новейших процессоров Intel.
Очень информативный блог на тему от одного из авторов Disruptor рекомендуется прочитать:
Всегда нужно спрашивать, зачем использовать Java, если вы хотите микрооптимизации, есть много альтернативных методов для ускорения функции, таких как использование JNA или JNI для передачи в нативную библиотеку.
источник