Гораздо сложнее «настроить» Java для повышения производительности по сравнению с C / C ++? [закрыто]

11

Мешает ли «магия» JVM влиянию программиста на микрооптимизации в Java? Я недавно читал на C ++, иногда упорядочение членов данных может обеспечить оптимизацию (предоставляется в микросекундной среде), и я предположил, что руки программиста связаны, когда дело доходит до сжатия производительности из Java?

Я ценю, что приличный алгоритм обеспечивает больший выигрыш в скорости, но если у вас есть правильный алгоритм, сложнее ли настроить Java из-за управления JVM?

Если нет, могут ли люди привести примеры того, какие приемы вы можете использовать в Java (помимо простых флагов компилятора).

user997112
источник
14
Основной принцип всей оптимизации Java заключается в следующем: JVM, вероятно, уже сделал это лучше, чем вы можете. Оптимизация в основном включает следование разумным методам программирования и избегание обычных вещей, таких как объединение строк в цикле.
Роберт Харви
3
Принцип микрооптимизации на всех языках заключается в том, что компилятор уже сделал это лучше, чем вы. Другой принцип микрооптимизации на всех языках заключается в том, что использование на нем большего количества оборудования обходится дешевле, чем микрооптимизация времени программиста. Программист должен стремиться к масштабированию проблем (неоптимальные алгоритмы), но микрооптимизация - пустая трата времени. Иногда микрооптимизация имеет смысл для встраиваемых систем, где вы не можете использовать больше оборудования, но Android, использующий Java, и его довольно слабая реализация показывают, что большинство из них уже имеют достаточно оборудования.
Ян Худек
1
для «трюкам производительности Java», заслуживает изучения являются: Effective Java , Angelika Langer Links - Java Performance и связанных с производительностью статьи Брайана Гетца в теории и практике Java и Threading легкомысленно серии перечисленное здесь
комар
2
Будьте предельно осторожны с советами и рекомендациями - JVM, операционные системы и аппаратное обеспечение продолжают развиваться - лучше всего изучать методологию настройки производительности и применять усовершенствования для вашей конкретной среды :-)
Martijn Verburg
В некоторых случаях виртуальная машина может выполнять оптимизации во время выполнения, которые нецелесообразно выполнять во время компиляции. Использование управляемой памяти может повысить производительность, хотя зачастую она также занимает больше места в памяти. Неиспользуемая память освобождается, когда это удобно, а не как можно скорее.
Брайан

Ответы:

5

Несомненно, на уровне микрооптимизации JVM будет делать некоторые вещи, которые вы будете иметь под небольшим контролем, особенно по сравнению с C и C ++.

С другой стороны, разнообразие поведений компилятора с C и C ++ особенно окажет гораздо большее негативное влияние на вашу способность выполнять микрооптимизацию любым неопределенно переносимым способом (даже между версиями компилятора).

Это зависит от того, какой проект вы настраиваете, какие среды вы нацеливаете и так далее. И, в конце концов, это не имеет большого значения, так как вы в любом случае получаете на несколько порядков лучшие результаты от алгоритмической / структуры данных / оптимизации дизайна программы.

Telastyn
источник
Это может иметь большое значение, когда вы обнаружите, что ваше приложение не масштабируется между ядрами
Джеймс
@james - хочешь уточнить?
Теластин
1
Смотрите здесь для начала: mechanical-sympathy.blogspot.co.uk/2011/07/false-sharing.html
Джеймс
1
@James, масштабирование между ядрами очень мало связано с языком реализации (за исключением Python!), И больше связано с архитектурой приложения.
Джеймс Андерсон
29

Микро-оптимизации почти никогда не стоят времени, и почти все простые выполняются автоматически компиляторами и средами выполнения.

Однако есть одна важная область оптимизации, где C ++ и Java принципиально отличаются, а именно массовый доступ к памяти. C ++ имеет ручное управление памятью, что означает, что вы можете оптимизировать структуру данных приложения и шаблоны доступа, чтобы в полной мере использовать кэши. Это довольно сложно, в некоторой степени относится к оборудованию, на котором вы работаете (поэтому прирост производительности может исчезнуть на другом оборудовании), но если все сделано правильно, это может привести к абсолютно захватывающей производительности. Конечно, вы платите за это с потенциалом для всех видов ужасных ошибок.

С таким языком сборки мусора, как Java, такого рода оптимизации невозможно выполнить в коде. Некоторые могут быть выполнены во время выполнения (автоматически или с помощью конфигурации, см. Ниже), а некоторые просто невозможны (цена, которую вы платите за защиту от ошибок управления памятью).

Если нет, могут ли люди привести примеры того, какие приемы вы можете использовать в Java (помимо простых флагов компилятора).

Флаги компилятора не имеют значения в Java, потому что компилятор Java практически не оптимизируется; время выполнения делает.

И действительно, среда выполнения Java имеет множество параметров, которые можно настроить, особенно в отношении сборщика мусора. В этих параметрах нет ничего «простого» - значения по умолчанию хороши для большинства приложений, а для повышения производительности необходимо точно понимать, что делают эти параметры и как работает ваше приложение.

Майкл Боргвардт
источник
1
+1: в основном то, что я писал в своем ответе, возможно, лучшая формулировка.
Klaim
1
+1: Очень хорошие моменты, объясненные очень кратко: «Это довольно сложно ... но если все сделано правильно, это может привести к совершенно захватывающим результатам. Конечно, вы платите за это с потенциалом для всех видов ужасных ошибок «.
Джорджио
1
@MartinBa: это больше, чем вы платите за оптимизацию управления памятью. Если вы не пытаетесь оптимизировать управление памятью, управление памятью в C ++ не так уж сложно (полностью избегайте его с помощью STL или сделайте его относительно простым с помощью RAII). Конечно, реализация RAII в C ++ требует больше строк кода, чем ничего не делая в Java (т. Е. Потому что Java обрабатывает это для вас).
Брайан
3
@ Мартин Ба: в основном да. Висячие указатели, переполнения буфера, неинициализированные указатели, ошибки в арифметике указателей, все вещи, которые просто не существуют без ручного управления памятью. И оптимизация доступа к памяти в значительной степени требует от вас сделать много ручного управление памятью.
Майкл Боргвардт
1
Есть несколько вещей, которые вы можете сделать в Java. Одним из них является пул объектов, который максимально увеличивает возможности памяти объектов (в отличие от C ++, где он может гарантировать локальность памяти).
RokL
5

[...] (предоставляется в микросекундной среде) [...]

Микросекунды складываются, если мы повторяем миллионы и миллиарды вещей. Персональный сеанс 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 в строку кэша, к которой можно получить доступ до выселения, поскольку в Integer4 раза больше, и это в лучшем случае.

И там есть много микрооптимизаций, поскольку мы объединены единой архитектурой / иерархией памяти. Шаблоны доступа к памяти не имеют значения, какой бы язык вы ни использовали, такие понятия, как разбиение на блоки / блокировка цикла, обычно могут применяться гораздо чаще в 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 (если только вы не использовали необработанный массив байтов или что-то в этом роде), но это для редких случаев, когда обашаблоны последовательного и произвольного доступа должны быть быстрыми, одновременно имея смесь типов полей для горячих полей. Для меня большая часть различий в стратегии оптимизации (на общем уровне) между этими двумя является спорным, если вы стремитесь к максимальной производительности.

Различия могут немного отличаться, если вы просто стремитесь к «хорошей» производительности - невозможность сделать что-либо с небольшими объектами, такими как Integervs., 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 ++, поскольку даже без этого основных накладных объекта и Разобщенная память, моделирование интерфейса на уровне отдельной частицы мешает наиболее эффективным решениям.

ChrisF
источник
1
Принимая во внимание, что плохая производительность в массе на самом деле может иметь приличную вероятность подавления максимальной производительности в критических областях, я не думаю, что можно полностью игнорировать преимущество хорошей производительности. И хитрость превращения массива структур в структуру массивов несколько ломается, когда все (или почти все) значения, входящие в одну из исходных структур, будут доступны одновременно. Кстати, я вижу, что вы раскопали много старых постов и добавили свой собственный хороший ответ, иногда даже хороший ;-)
Deduplicator
1
@Deduplicator Надеюсь, я не раздражаю людей, наталкивая их слишком сильно! Этот получил немного крошечную болтовню - возможно, я должен немного улучшить это. SoA против AoS часто бывает сложным (последовательный или произвольный доступ). Я редко знаю заранее, какой из них использовать, поскольку в моем случае часто используется сочетание последовательного и произвольного доступа. Ценный урок, который я часто усваиваю, заключается в разработке интерфейсов, которые оставляют достаточно места для игры с представлением данных - своего рода более объемные интерфейсы, которые, когда это возможно, имеют большие алгоритмы преобразования (иногда это невозможно при случайном доступе крошечным битам).
1
Ну, я заметил только потому, что все идет очень медленно. И я не торопился с каждым.
Дедупликатор
Мне действительно интересно, почему user204677ушел. Такой отличный ответ.
oligofren
3

Существует средняя область между микрооптимизацией, с одной стороны, и хорошим выбором алгоритма, с другой.

Это область ускорений с постоянным коэффициентом, и она может давать порядки величин.
Это делается путем отсечения целых долей времени выполнения, таких как сначала 30%, затем 20% того, что осталось, затем 50% этого и так далее в течение нескольких итераций, пока почти ничего не останется.

Вы не видите этого в маленьких программах в демо-стиле. Вы видите это в больших серьезных программах с большим количеством структур данных классов, где стек вызовов обычно имеет многоуровневую структуру. Хороший способ найти возможности ускорения - это изучить случайные выборки состояния программы.

Обычно ускорения состоят из таких вещей, как:

  • сведение к минимуму вызовов newпутем объединения и повторного использования старых объектов,

  • признавая, что делается что-то вроде ради общности, а не на самом деле необходимо,

  • пересмотр структуры данных с использованием различных классов сбора, которые имеют одинаковое поведение big-O, но используют преимущества фактически используемых шаблонов доступа,

  • сохранение данных, полученных вызовами функций, вместо повторного вызова функции (это естественная и забавная тенденция программистов предполагать, что функции с более короткими именами выполняются быстрее.)

  • допуская определенную степень несоответствия между избыточными структурами данных, вместо того, чтобы пытаться поддерживать их в полном соответствии с событиями уведомлений,

  • и т. д.

Но, конечно, ни одна из этих вещей не должна быть сделана без проблем с отбором проб.

Майк Данлавей
источник
2

Java (насколько я знаю) не дает вам контроля над расположением переменных в памяти, поэтому вам труднее избежать таких вещей, как ложное разделение и выравнивание переменных (вы можете добавить класс с несколькими неиспользуемыми членами). Еще одна вещь, которую я не думаю, что вы можете воспользоваться такими инструкциями, как mmpause, но эти вещи зависят от процессора, и поэтому, если вы считаете, что вам это нужно, Java может не быть языком для использования.

Существует класс Unsafe, который дает вам гибкость C / C ++, но также и с опасностью C / C ++.

Это может помочь вам взглянуть на ассемблерный код, который JVM генерирует для вашего кода.

Чтобы прочитать о Java-приложении, которое рассматривает такие подробности, смотрите код Disruptor, выпущенный LMAX

Джеймс
источник
2

На этот вопрос очень сложно ответить, потому что это зависит от языковых реализаций.

В общем, сейчас очень мало места для такой «микрооптимизации». Основная причина в том, что компиляторы используют преимущества такой оптимизации во время компиляции. Например, в ситуациях, когда их семантика идентична, нет никакой разницы в производительности между операторами до увеличения и после увеличения. Другим примером может служить, например, такой цикл, в for(int i=0; i<vec.size(); i++)котором можно утверждать, что вместо вызоваsize()Функция-член во время каждой итерации было бы лучше получить размер вектора перед циклом, а затем сравнить с этой единственной переменной и, таким образом, избежать вызова функции за одну итерацию. Однако в некоторых случаях компилятор обнаруживает этот глупый случай и кэширует результат. Однако это возможно только в том случае, если функция не имеет побочных эффектов, и компилятор может быть уверен, что размер вектора остается постоянным в течение цикла, поэтому он применяется только к довольно тривиальным случаям.

zxcdw
источник
Что касается второго случая, я не думаю, что компилятор сможет оптимизировать его в обозримом будущем. Обнаружение того, что оптимизация vec.size () безопасна, зависит от доказательства того, что размер, если вектор / потерян, не изменяется внутри цикла, что я считаю неразрешимым из-за проблемы остановки.
Ли Райан
@LieRyan Я видел множество (простых) случаев, когда компилятор генерировал точно такой же двоичный файл, если результат был вручную «кэширован» и если вызывался size (). Я написал некоторый код, и оказалось, что поведение сильно зависит от того, как работает программа. В некоторых случаях компилятор может гарантировать отсутствие возможности изменения размера вектора во время цикла, а также в некоторых случаях он не может этого гарантировать, очень похоже на проблему остановки, как вы упоминали. Пока я не могу подтвердить свою претензию (разборка C ++ - это боль), поэтому я отредактировал ответ
zxcdw
2
@Lie Ryan: многие вещи, которые неразрешимы в общем случае, вполне разрешимы для конкретных, но общих случаев, и это действительно все, что вам нужно здесь.
Майкл Боргвардт
@LieRyan Если вы вызываете только constметоды для этого вектора, я уверен, что многие оптимизирующие компиляторы это поймут.
Стефф
в C #, и я думаю, что я также читаю на Java, если вы не определяете размер кэша, компилятор знает, что он может удалить проверки, чтобы увидеть, выходите ли вы за пределы массива, и если вы делаете размер кэша, он должен выполнить проверки , которые обычно стоят больше, чем вы экономите путем кэширования. Попытка перехитрить оптимизаторов - редко хороший план.
Кейт Грегори
1

Могли бы люди привести примеры того, какие приемы вы можете использовать в Java (помимо простых флагов компилятора).

Помимо улучшения алгоритмов, обязательно рассмотрим иерархию памяти и как процессор использует его. Снижение задержек при доступе к памяти дает большие преимущества, когда вы понимаете, как рассматриваемый язык распределяет память по типам данных и объектам.

Пример Java для доступа к массиву 1000x1000 дюймов

Рассмотрим приведенный ниже пример кода - он обращается к той же области памяти (массив целых 1000x1000), но в другом порядке. На моем Mac mini (Core i7, 2,7 ГГц) вывод выглядит следующим образом, показывая, что обход массива по строкам более чем удваивает производительность (в среднем более 100 раундов каждый).

Processing columns by rows*** took 4 ms (avg)
Processing rows by columns*** took 10 ms (avg) 

Это связано с тем, что массив хранится таким образом, что последовательные столбцы (т. Е. Значения int) размещаются рядом в памяти, а последовательные строки - нет. Чтобы процессор действительно использовал данные, они должны быть переданы в свои кэши. Передача памяти осуществляется блоком байтов, называемым строкой кэша - загрузка строки кэша непосредственно из памяти приводит к задержкам и, таким образом, снижает производительность программы.

Для Core i7 (песчаный мост) строка кэша содержит 64 байта, таким образом, каждый доступ к памяти извлекает 64 байта. Поскольку первый тест обращается к памяти в предсказуемой последовательности, процессор будет предварительно извлекать данные до того, как они будут фактически использованы программой. В целом, это приводит к меньшей задержке при обращении к памяти и, таким образом, повышает производительность.

Код образца:

  package test;

  import java.lang.*;

  public class PerfTest {
    public static void main(String[] args) {
      int[][] numbers = new int[1000][1000];
      long startTime;
      long stopTime;
      long elapsedAvg;
      int tries;
      int maxTries = 100;

      // process columns by rows 
      System.out.print("Processing columns by rows");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int r = 0; r < 1000; r++) {
         for(int c = 0; c < 1000; c++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     

      // process rows by columns
      System.out.print("Processing rows by columns");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int c = 0; c < 1000; c++) {
         for(int r = 0; r < 1000; r++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     
    }
  }
miraculixx
источник
1

JVM может и часто создает помехи, и JIT-компилятор может значительно меняться между версиями. Некоторые микрооптимизации невозможны в Java из-за языковых ограничений, таких как дружественность к гиперпоточности или коллекция SIMD новейших процессоров Intel.

Очень информативный блог на тему от одного из авторов Disruptor рекомендуется прочитать:

Всегда нужно спрашивать, зачем использовать Java, если вы хотите микрооптимизации, есть много альтернативных методов для ускорения функции, таких как использование JNA или JNI для передачи в нативную библиотеку.

Стив-О
источник