Эффективность кода Java с примитивными типами

Я хочу спросить, какой фрагмент кода на Java более эффективен? Код 1:

void f()
{
 for(int i = 0 ; i < 99999;i++)
 {
  for(int j = 0 ; j < 99999;j++)
  {
   //Some operations
  }
 }

}

Код 2:

void f()
{
 int i,j;
 for(i = 0 ; i < 99999;i++)
 {
  for(j = 0 ; j < 99999;j++)
  {
   //Some operations
  }
 }

}

Мой учитель сказал, что второй лучше, но я не могу согласиться с этим мнением.


person piotrek89    schedule 07.05.2010    source источник
comment
Вы можете провести тест самостоятельно с обоими кодами (обычно с числом больше 99999) и рассчитать время, затраченное на запуск кода. Что касается меня, я стараюсь использовать первый метод, если я не буду использовать i, j вне for. В идеале также первый метод будет удалять переменные типа int после завершения цикла.   -  person Random    schedule 07.05.2010
comment
javac компилирует их в один и тот же код. Пожалуйста, не позволяйте своему учителю говорить такую ​​чушь, иначе он снова научит этому других учеников в будущем.   -  person Oak    schedule 07.05.2010
comment
Это твой учитель латыни? История, химия, что?   -  person Kevin Bourrillion    schedule 07.05.2010
comment
Кроме того, в целом считается, что в высококачественном коде объявления переменных должны располагаться как можно ближе к области, в которой они выполняются. Я очень надеюсь, что ваш учитель сказал, что это будет более эффективно (возможно, тоже неправильно, но менее ошибочно), чем лучше. Почти повсеместно было бы считаться худшей практикой делать это таким образом, независимо от эффективности, особенно в Java.   -  person kingfrito_5005    schedule 17.01.2017


Ответы (9)


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

Другие ответы тоже верны: не беспокойтесь о таких вещах для производительности. Но обязательно подумайте об этом по причинам, связанным с удобочитаемостью кода, и для передачи намерений программиста следующему человеку. Это гораздо важнее, чем забота о микрооптимизации.

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

Если вы не уверены, вы всегда можете скомпилировать его и посмотреть, что произойдет. Сделаем два класса, f1 и f2, для двух версий:

$ cat f1.java
public class f1 {
  void f() {
    for(int i = 0 ; i < 99999;i++) {
      for(int j = 0 ; j < 99999;j++) {
      }
    }
  }
}

$ cat f2.java
public class f2 {
  void f() {
    int i, j;
    for(i = 0 ; i < 99999;i++) {
      for(j = 0 ; j < 99999;j++) {
      }
    }
  }
}

Скомпилируйте их:

$ javac f1.java
$ javac f2.java

И декомпилируйте их:

$ javap -c f1 > f1decomp
$ javap -c f2 > f2decomp

И сравните их:

$ diff f1decomp f2decomp
1,3c1,3
< Compiled from "f1.java"
< public class f1 extends java.lang.Object{
< public f1();
---
> Compiled from "f2.java"
> public class f2 extends java.lang.Object{
> public f2();

В байт-коде нет абсолютно никакой разницы.

person Anonymoose    schedule 07.05.2010
comment
это Booyaah, in your face teacher!! ответ, прямо здесь. :П - person st0le; 30.12.2010

ЭТО. НЕТ. СДЕЛАТЬ. РАЗНИЦА.

Прекратите микрооптимизацию. Эти маленькие уловки не заставляют программы работать намного быстрее.

Сконцентрируйтесь на оптимизации общей картины и написании удобочитаемого кода.

Объявляйте переменные там, где они имеют смысл и где это помогает понять семантику всего кода в более широком контексте, а не потому, что вы думаете, что они быстрее в одном месте над другим.

person polygenelubricants    schedule 07.05.2010
comment
Согласованный! Вы должны сосредоточиться на том, почему вы выполняете вложенный цикл и какие операции выполняются ВНУТРИ вложенного цикла. Только не две тупые тупицы снаружи! - person basszero; 07.05.2010
comment
Тем не менее, вопрос остается очень интересным. Почему компилятор Java не оптимизирует оба цикла, потому что у них нет побочных эффектов? Даже если // some operations был чем-то большим, чем комментарий, это простая микрооптимизация для javac, поэтому оба варианта должны быть одинаково быстрыми. - person Alexandru; 07.05.2010
comment
@Alexandru Я не слежу - если // какие-то операции - это не просто комментарий, как вы рассчитываете их оптимизировать? - person CPerkins; 07.05.2010
comment
for (int i = 0; i < 999999; ++i) {} бесполезен и может быть удален. Стивен С. уже указывал на это в другом ответе, хотя я озадачен, почему java предпочитает большую оптимизацию во время выполнения и меньше во время компиляции. - person Alexandru; 07.05.2010
comment
@Alexandru: .java в байт-код не оптимизирован. Наиболее эффективная оптимизация происходит на уровне JIT, а не на уровне javac. - person polygenelubricants; 07.05.2010
comment
Я согласен с идеей не микрооптимизации, но здесь важно одно: область видимости переменных. Это может создать настоящие проблемы. - person cletus; 07.05.2010
comment
@cletus: предположительно, два варианта представлены в контексте, в котором они семантически совместимы (или это спорный вопрос, какой из них быстрее), поэтому область видимости переменных не имеет значения для правильности. Конечно, это ОЧЕНЬ важно для удобочитаемости, и вместо того, чтобы указывать, какой из них лучше, и на этом закончить, я чувствую, что OP должен разобраться в этом самостоятельно. Надеюсь, в процессе OP узнает, что действительно важно. - person polygenelubricants; 07.05.2010
comment
@Alexandru - Я понимаю, что если это пустой цикл, его нужно оптимизировать. Я спрашиваю о вашем комментарии выше. Даже если // некоторые операции были чем-то большим, чем комментарий ...: итак, если там есть код, и это больше, чем комментарий, что, по вашему мнению, будет оптимизировано? О какой микрооптимизации вы говорите? - person CPerkins; 07.05.2010
comment
Отладка @Alexandru, быстрая компиляция, оптимизация, зависящая от платформы, оптимизация на основе использования - вот некоторые причины. - person josefx; 07.05.2010
comment
@CPerkins Извините за непонятное начало. Показанная вами микрооптимизация (перемещение i, j из цикла) должна выполняться java легко (и это делается с помощью JVM). - person Alexandru; 07.05.2010

Остерегайтесь опасностей микробенчмаркинга !!!

Я взял код, обернул метод снаружи и запустил его 10 раз в цикле. Результаты:

50, 3, 
3, 0, 
0, 0, 
0, 0, 
....

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

Урок 1: Компиляторы часто оптимизируют код, который выполняет бесполезную «работу». Чем умнее компилятор, тем больше вероятность того, что подобное произойдет. Если вы не учитываете это в способе кодирования, эталонный тест может быть бессмысленным.

Затем я добавил следующее простое вычисление в оба цикла if (i < 2 * j) longK++; и заставил тестовый метод возвращать окончательное значение longK. Результаты:

32267, 33382,
34542, 30136,
12893, 12900,
12897, 12889,
12904, 12891,
12880, 12891,
....

Очевидно, мы остановили компиляторы, оптимизирующие цикл. Но теперь мы видим эффекты разогрева JVM (в данном случае) первых двух пар итераций цикла. Первые две пары итераций (один вызов метода), вероятно, выполняются исключительно в интерпретируемом режиме. И похоже, что третья итерация может работать параллельно с JIT. К третьей паре итераций мы, скорее всего, будем запускать чистый собственный код. И с тех пор разница между синхронизацией двух версий цикла - это просто шум.

Урок 2: всегда учитывайте эффект прогрева JVM. Это может серьезно исказить результаты тестов, как на микро, так и на макроуровне.

Заключение - как только JVM разогревается, между двумя версиями цикла нет заметной разницы.

person Stephen C    schedule 07.05.2010
comment
+1 за хорошее объяснение, пример эффектов внутрипроцессной оптимизации JVM / JIT. Моя собственная мысль, люди часто, кажется, забывают, что скомпилированный код не обязательно должен быть дословно написанным кодом, если он вычислительно эквивалентен, то есть классический пример рекурсивной фибонначи, переведенной в простой цикл умным компилятором (еще до того, как он дойдет до JVM / JIT). - person M. Jessup; 07.05.2010
comment
Я почти уверен, что компилятор (javac) НЕ отвечает за оптимизацию первых результатов (ноль раз). Это выполняется JVM: у меня были нули только при работе с параметром -server, в противном случае - нормальное раз (около 10 с). Проверка байтового кода не показывает никакой оптимизации (несмотря на повторное использование одного и того же адреса для локальных переменных для обоих циклов, независимо от того, где они были объявлены) - person user85421; 10.05.2010

Второй хуже.

Почему? Поскольку переменная цикла находится вне цикла. i и j будут иметь значение после завершения цикла. Обычно это не то, что вам нужно. Первый охватывает переменные цикла, поэтому он виден только внутри цикла.

person cletus    schedule 07.05.2010
comment
Согласен - любое повышение эффективности не стоит возможных проблем из-за повторного использования i и j. Однако, как ни странно, в C ++ не все компиляторы применяют область видимости i и j, если они объявлены в цикле. - person Robben_Ford_Fan_boy; 07.05.2010
comment
@Marcelo - @cletus никогда ничего не говорил об ошибках компиляции. - person Stephen C; 07.05.2010
comment
Стивен, он сделал это в первой версии. Я не знаю, почему это не отображается как правка, но я это видел. Замечание было, что вторая версия даже не компилируется. - person Marcelo Cantos; 07.05.2010
comment
Это редактировалось? Оба кода отлично работают в моем затмении ... Изменить: хорошо, хорошо - person Random; 07.05.2010

Я бы предположил, что нет абсолютно никакой разницы в эффективности для любой полуприличной реализации JVM.

person Marcelo Cantos    schedule 07.05.2010

Нет, это вообще не имеет значения (по скорости). Оба они компилируются в один и тот же код. И не происходит никакого распределения и освобождения, как сказал MasterGaurav.

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

Единственная небольшая крошечная незначительная разница (кроме области видимости) заключается в том, что в первом примере память, выделенная для i и j, может быть повторно использована для других переменных. Следовательно, JVM будет выделять меньше слотов памяти для этого метода (ну, вы сохранили некоторые биты)

person H-H    schedule 07.05.2010

Во-первых, да, ваш учитель ошибается, второй код не лучше. Что значит лучше? Это связано с тем, что в любом нормальном цикле операции внутри тела цикла являются частью, отнимающей много времени. Таким образом, Code 2 - это просто микро-оптимизация, которая не увеличивает скорость (если таковая имеется), чтобы оправдать плохую читаемость кода.

person ablaeul    schedule 07.05.2010

Второй лучше по скорости.

Причина в том, что в первом случае область действия j ограничена внутренним циклом for.

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

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

person Gaurav Vaish    schedule 07.05.2010
comment
Не существует такой вещи, как выделение стека. Код просто использует определенную часть доступного стека для хранения int. - person Marcelo Cantos; 07.05.2010
comment
Да, ответ неверный. В JVM не происходит выделения и освобождения - person H-H; 07.05.2010
comment
В первом случае переменная j выходит за пределы области видимости ... Итак, что это значит? Я согласен, что нет ничего лучше, чем распределение стека ... но затем, используя значение в стеке и выводя его за рамки ... это только материал времени компиляции? - person Gaurav Vaish; 07.05.2010
comment
МастерГаурав, я объяснил это в своем ответе. JVM выделяет достаточно памяти для всех локальных переменных. Когда локальная переменная выходит за пределы области видимости, тот же слот памяти используется для другой локальной переменной. - person H-H; 07.05.2010
comment
Спасибо HH! Моя ошибка понимания :) - person Gaurav Vaish; 07.05.2010

Есть еще один аспект, который очень важен для двух разных версий:

В то время как в варианте 2 создаются только два временных объекта (i и j), вариант 1 создает 100000 объектов (1 x i и 999999 x j). Возможно, ваш компилятор это оптимизирует, но вы не можете быть уверены в этом. Если его не оптимизировать, сборка мусора будет вести себя значительно хуже.

person Georg Edelmann    schedule 07.05.2010