Я новичок в Java, и прошлой ночью выполнял какой-то код, и это действительно беспокоило меня. Я строил простую программу для отображения всех выходов X в цикле for, и я заметил МАССОВОЕ снижение производительности, когда я использовал модуль как variable % variable
vs variable % 5000
или еще много чего. Может кто-нибудь объяснить мне, почему это происходит и чем это вызвано? Так что я могу быть лучше ...
Вот «эффективный» код (извините, если я неправильно понял синтаксис, я сейчас не на компьютере с кодом)
long startNum = 0;
long stopNum = 1000000000L;
for (long i = startNum; i <= stopNum; i++){
if (i % 50000 == 0) {
System.out.println(i);
}
}
Вот "неэффективный код"
long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 50000;
for (long i = startNum; i <= stopNum; i++){
if (i % progressCheck == 0) {
System.out.println(i);
}
}
Имейте в виду, у меня была переменная даты для измерения различий, и как только она стала достаточно длинной, первая заняла 50 мс, а другая - 12 секунд или что-то в этом роде. Возможно, вам придется увеличить stopNum
или уменьшить, progressCheck
если ваш компьютер более эффективен, чем мой, или нет.
Я искал этот вопрос в Интернете, но не могу найти ответ, может быть, я просто не правильно его спрашиваю.
РЕДАКТИРОВАТЬ: Я не ожидал, что мой вопрос будет настолько популярным, я ценю все ответы. Я выполнил эталонный тест по каждой полученной половине времени, и неэффективный код занял значительно больше времени, 1/4 секунды по сравнению с 10 секундами отдача или взятие. Конечно, они используют println, но они оба делают одинаковое количество, так что я бы не подумал, что это сильно исказит его, особенно если расхождение повторяется. Что касается ответов, так как я новичок в Java, я позволю голосам решить, какой ответ лучше. Я постараюсь выбрать один к среде.
РЕДАКТИРОВАТЬ 2: Я собираюсь сделать еще один тест сегодня вечером, где вместо модуля, он просто увеличивает переменную, а когда он достигает progressCheck, он выполнит ее, а затем сбросит эту переменную на 0. для третьего варианта.
EDIT3.5:
Я использовал этот код, и ниже я покажу свои результаты. Спасибо ВСЕМ за прекрасную помощь! Я также попытался сравнить короткое значение long с 0, поэтому все мои новые проверки выполняются когда-либо «65536» раз, делая его равным в повторениях.
public class Main {
public static void main(String[] args) {
long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 65536;
final long finalProgressCheck = 50000;
long date;
// using a fixed value
date = System.currentTimeMillis();
for (long i = startNum; i <= stopNum; i++) {
if (i % 65536 == 0) {
System.out.println(i);
}
}
long final1 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
//using a variable
for (long i = startNum; i <= stopNum; i++) {
if (i % progressCheck == 0) {
System.out.println(i);
}
}
long final2 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
// using a final declared variable
for (long i = startNum; i <= stopNum; i++) {
if (i % finalProgressCheck == 0) {
System.out.println(i);
}
}
long final3 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
// using increments to determine progressCheck
int increment = 0;
for (long i = startNum; i <= stopNum; i++) {
if (increment == 65536) {
System.out.println(i);
increment = 0;
}
increment++;
}
//using a short conversion
long final4 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
for (long i = startNum; i <= stopNum; i++) {
if ((short)i == 0) {
System.out.println(i);
}
}
long final5 = System.currentTimeMillis() - date;
System.out.println(
"\nfixed = " + final1 + " ms " + "\nvariable = " + final2 + " ms " + "\nfinal variable = " + final3 + " ms " + "\nincrement = " + final4 + " ms" + "\nShort Conversion = " + final5 + " ms");
}
}
Полученные результаты:
- исправлено = 874 мс (обычно около 1000 мс, но быстрее из-за мощности 2)
- переменная = 8590 мс
- конечная переменная = 1944 мс (было ~ 1000 мс при использовании 50000)
- приращение = 1904 мс
- Короткое преобразование = 679 мс
Неудивительно, что из-за недостатка деления «Короткая конверсия» была на 23% быстрее, чем «быстрый» путь. Это интересно отметить. Если вам нужно что-то показывать или сравнивать каждые 256 раз (или примерно там), вы можете сделать это и использовать
if ((byte)integer == 0) {'Perform progress check code here'}
ОДНО ЗАКЛЮЧИТЕЛЬНОЕ ЗАИНТЕРЕСОВАННОЕ ПРИМЕЧАНИЕ: использование модуля в «Окончательной объявленной переменной» с 65536 (не довольно большое число) было вдвое быстрее (медленнее), чем фиксированное значение. Где раньше это было тестирование примерно с той же скоростью.
источник
final
передprogressCheck
переменной, оба снова будут работать с одинаковой скоростью. Это приводит меня к мысли, что компилятору или JIT удается оптимизировать цикл, когда он знает, чтоprogressCheck
он постоянен.Ответы:
Вы измеряете заглушку OSR (замена в стеке) .
Заглушка OSR - это специальная версия скомпилированного метода, предназначенная специально для переноса выполнения из интерпретируемого режима в скомпилированный код во время работы метода.
Заглушки OSR не так оптимизированы, как обычные методы, потому что им требуется структура кадра, совместимая с интерпретируемым кадром. Я показал это уже в следующих ответах: 1 , 2 , 3 .
Подобное происходит и здесь. Пока «неэффективный код» выполняет длинный цикл, метод компилируется специально для замены в стеке прямо внутри цикла. Состояние передается из интерпретируемого фрейма в метод, скомпилированный OSR, и это состояние включает
progressCheck
локальную переменную. В этот момент JIT не может заменить переменную константой и, следовательно, не может применить определенные оптимизации, такие как снижение прочности .В частности, это означает, что JIT не заменяет целочисленное деление на умножение . (См. Почему GCC использует умножение на странное число при реализации целочисленного деления? Для трюка asm от опережающего компилятора, когда значение является константой времени компиляции после встраивания / распространения констант, если эти оптимизации включены Целочисленное буквальное право в
%
выражении также оптимизируетсяgcc -O0
, как здесь, где оно оптимизируется JITer даже в заглушке OSR.)Однако, если вы запускаете один и тот же метод несколько раз, второй и последующие запуски выполнят обычный (не OSR) код, который полностью оптимизирован. Вот тест для доказательства теории ( с использованием JMH ):
И результаты:
Самая первая итерация
divVar
действительно намного медленнее из-за неэффективно скомпилированной заглушки OSR. Но как только метод перезапускается с самого начала, запускается новая неограниченная версия, которая использует все доступные оптимизации компилятора.источник
System.out.println
почти обязательно приведет к ошибочным результатам, и тот факт, что обе версии одинаково быстры, не имеет ничего общего с OSR, в частности , насколько я могу судить ..1
немного сомнительна - пустой цикл также можно полностью оптимизировать. Второй больше похож на этот. Но опять же , это не понятно , почему вы приписываете разницу в OSR конкретно . Я бы просто сказал: в какой-то момент метод JITed и становится быстрее. Насколько я понимаю, OSR только приводит к тому, что использование окончательного, оптимизированного кода (примерно) будет «отложено до следующего этапа оптимизации». (продолжение ...)%
операция имела бы одинаковый вес, поскольку оптимизированное выполнение возможно только в том случае, если бы оптимизатор выполнил реальную работу. Таким образом, тот факт, что один вариант цикла значительно быстрее другого, доказывает наличие оптимизатора и еще раз доказывает, что он не смог оптимизировать один из циклов в той же степени, что и другой (в рамках одного и того же метода!). Поскольку этот ответ доказывает способность оптимизировать оба цикла в одинаковой степени, должно быть что-то, что препятствовало оптимизации. И это ЛРН в 99,9% всех случаев-XX:+PrintCompilation -XX:+TraceNMethodInstalls
.В продолжение комментария @phuclv я проверил код, сгенерированный JIT 1 , результаты следующие:
для
variable % 5000
(деление на постоянное):для
variable % variable
:Поскольку деление всегда занимает больше времени, чем умножение, последний фрагмент кода менее производительный.
Версия Java:
1 - используемые параметры виртуальной машины:
-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,src/java/Main.main
источник
imul
это 3 цикла,idiv
это между 30 и 90 циклами. Таким образом, целочисленное деление происходит в 10–30 раз медленнее, чем целочисленное умножение.Как уже отмечали другие, операция общего модуля требует деления. В некоторых случаях деление может быть заменено (компилятором) умножением. Но оба могут быть медленными по сравнению с сложением / вычитанием. Следовательно, лучшая производительность может ожидаться чем-то вроде этого:
(В качестве незначительной попытки оптимизации мы используем здесь предварительный декремент обратного счетчика, потому что на многих архитектурах по сравнению с
0
сразу после арифметической операции стоит ровно 0 инструкций / циклов ЦП, поскольку флаги ALU уже установлены соответствующим образом предыдущей операцией. Достойная оптимизация однако компилятор выполнит эту оптимизацию автоматически, даже если вы напишитеif (counter++ == 50000) { ... counter = 0; }
.)Обратите внимание, что часто вам не нужен / не нужен модуль, потому что вы знаете, что ваш счетчик цикла (
i
) или что-то еще увеличивается только на 1, и вы действительно не заботитесь о фактическом остатке, который даст вам модуль, просто посмотрите если счетчик увеличения на единицу достигает некоторого значения.Другой «трюк» заключается в использовании значений / пределов степени двух, например
progressCheck = 1024;
. Modulus степень двойки может быть быстро рассчитывается с помощью побитовогоand
, то естьif ( (i & (1024-1)) == 0 ) {...}
. Это тоже должно быть довольно быстро, и на некоторых архитектурах может превзойти явноеcounter
выше.источник
if()
Тело становится телом внешнего контура, а материал снаружиif()
становится внутренним телом цикла , который выполняется дляmin(progressCheck, stopNum-i)
итераций. Таким образом, в начале, и каждый раз, когдаcounter
достигает 0, вы делаетеlong next_stop = i + min(progressCheck, stopNum-i);
настройку дляfor(; i< next_stop; i++) {}
цикла. В этом случае этот внутренний цикл пуст и, как мы надеемся, должен полностью оптимизироваться, вы можете сделать это в источнике и упростить JITer, сократив ваш цикл до i + = 50k.--counter
оно так же быстро, как и моя версия приращения, но меньше кода. Кроме того, оно было на 1 ниже, чем должно быть, мне интересно, если это должно быть,counter--
чтобы получить точное число, которое вы хотите Не то, чтобы это было большой разницей--counter
выключен одним.counter--
даст вам точноеprogressCheck
количество итераций (или вы могли бы установить,progressCheck = 50001;
конечно).Я также удивлен, увидев производительность вышеуказанных кодов. Это все о времени, затраченном компилятором на выполнение программы согласно объявленной переменной. Во втором (неэффективном) примере:
Вы выполняете операцию модуля между двумя переменными. Здесь компилятор должен проверять значение
stopNum
иprogressCheck
переходить к конкретному блоку памяти, расположенному для этих переменных, каждый раз после каждой итерации, потому что это переменная и ее значение может быть изменено.Вот почему после каждой итерации компилятор отправлялся в область памяти, чтобы проверить последние значения переменных. Поэтому во время компиляции компилятор не смог создать эффективный байт-код.
В первом примере кода вы выполняете оператор модуля между переменной и постоянным числовым значением, которое не будет изменяться в процессе выполнения, и компилятору не нужно проверять значение этого числового значения из области памяти. Вот почему компилятор смог создать эффективный байт-код. Если вы объявляете
progressCheck
какfinal
или в качествеfinal static
переменной , то в момент времени выполнения / компиляции компилятор знаю , что это конечная величина и ее значение не изменится , то компилятор заменитprogressCheck
с50000
в коде:Теперь вы можете видеть, что этот код также выглядит как первый (эффективный) пример кода. Производительность первого кода и, как мы упоминали выше, оба кода будут работать эффективно. Не будет большой разницы во времени выполнения любого примера кода.
источник
volatile
«компилятор» не будет снова и снова считывать свое значение из ОЗУ.