public static void main(final String[] args) {
final List<String> myList = Arrays.asList("A", "B", "C", "D");
final long start = System.currentTimeMillis();
for (int i = 1000000; i > myList.size(); i--) {
System.out.println("Hello");
}
final long stop = System.currentTimeMillis();
System.out.println("Finish: " + (stop - start));
}
vs
public static void main(final String[] args) {
final List<String> myList = Arrays.asList("A", "B", "C", "D");
final long start = System.currentTimeMillis();
final int size = myList.size();
for (int i = 1000000; i > size; i--) {
System.out.println("Hello");
}
final long stop = System.currentTimeMillis();
System.out.println("Finish: " + (stop - start));
}
Будет ли это иметь значение? На моей машине второй работает быстрее, но я не знаю, действительно ли он точен. Будет ли компилятор оптимизировать этот код? Я мог подумать, что он будет, если условие цикла является неизменяемым объектом (например, массивом строк).
Если вы хотите измерить прошедшее время (и хотите, чтобы оно было точным), вам следует использовать System.nanoTime() BTW. Pascal Thivent 05.03.2010
Если вы хотите протестировать что-то подобное, вам действительно нужно оптимизировать свой микротест, чтобы измерять то, что вас волнует.
Во-первых, сделайте цикл недорогим, но который невозможно пропустить. Вычисление суммы обычно помогает.
Во-вторых, сравните два тайминга.
Вот некоторый код, который делает оба:
import java.util.*;
public class Test {
public static long run1() {
final List<String> myList = Arrays.asList("A", "B", "C", "D");
final long start = System.nanoTime();
int sum = 0;
for (int i = 1000000000; i > myList.size(); i--) sum += i;
final long stop = System.nanoTime();
System.out.println("Finish: " + (stop - start)*1e-9 + " ns/op; sum = " + sum);
return stop-start;
}
public static long run2() {
final List<String> myList = Arrays.asList("A", "B", "C", "D");
final long start = System.nanoTime();
int sum = 0;
int limit = myList.size();
for (int i = 1000000000; i > limit; i--) sum += i;
final long stop = System.nanoTime();
System.out.println("Finish: " + (stop - start)*1e-9 + " ns/op; sum = " + sum);
return stop-start;
}
public static void main(String[] args) {
for (int i=0 ; i<5 ; i++) {
long t1 = run1();
long t2 = run2();
System.out.println(" Speedup = " + (t1-t2)*1e-9 + " ns/op\n");
}
}
}
И если мы запустим его, в моей системе мы получим:
Finish: 0.481741256 ns/op; sum = -243309322
Finish: 0.40228402 ns/op; sum = -243309322
Speedup = 0.079457236 ns/op
Finish: 0.450627151 ns/op; sum = -243309322
Finish: 0.43534661700000005 ns/op; sum = -243309322
Speedup = 0.015280534 ns/op
Finish: 0.47738474700000005 ns/op; sum = -243309322
Finish: 0.403698331 ns/op; sum = -243309322
Speedup = 0.073686416 ns/op
Finish: 0.47729349600000004 ns/op; sum = -243309322
Finish: 0.405540508 ns/op; sum = -243309322
Speedup = 0.071752988 ns/op
Finish: 0.478979617 ns/op; sum = -243309322
Finish: 0.36067492700000003 ns/op; sum = -243309322
Speedup = 0.11830469 ns/op
что означает, что накладные расходы на вызов метода составляют примерно 0,1 нс. Если ваша петля выполняет действия, которые занимают не более 1-2 нс, вам следует позаботиться об этом. В противном случае, не делайте этого.
Ответ 2 · рейтинг 10 · TofuBeer
Однажды я работал над проектом, где моей первой задачей было отследить какой-то безумно медленный код (это было на совершенно новой машине 486, и его выполнение заняло около 20 минут):
for(size_t i = 0; i < strlen(data); i++)
{
// do something with data[i]
}
Решение было (сократилось примерно до двух минут или меньше):
size_t length = strlen(data);
for(int i = 0; i < length; i++)
{
// do something with data[i]
}
Проблема в том, что "данные" превышают 1 миллион символов, и strlen должен постоянно считать каждый из них.
В случае Java метод "size()", вероятно, возвращает переменную, и поэтому виртуальная машина будет ее встраивать. На виртуальной машине, такой как на Android, это, вероятно, не так. Так что ответ "это зависит".
Лично я предпочитаю никогда не вызывать метод более одного раза, если он должен каждый раз возвращать один и тот же результат. Таким образом, если метод включает в себя расчет, он выполняется только один раз, и тогда это никогда не будет проблемой.
Ответ 3 · рейтинг 10 · duffymo
Лично я не думаю, что вы можете сделать какие-либо осмысленные выводы из такого надуманного примера.
Но если вы действительно хотите знать, почему бы не использовать javap для декомпиляции кода и посмотреть, что изменилось? Зачем догадываться о том, что делает компилятор, когда вы можете сами убедиться, не спрашивая здесь?
Различия есть, но я не уверен, что могу однозначно сказать об их влиянии на производительность.
Я бы закодировал второй, потому что это означало бы (на первый взгляд) один вызов метода, а не один на итерацию цикла. Я не знаю, сможет ли компилятор оптимизировать это, но я уверен, что смогу сделать это довольно легко. Я так и делаю, независимо от того, как это влияет на время у стены.
Ответ 4 · рейтинг 5 · Thomas Pornin
Обратите внимание, что компилятор javac не имеет ничего общего с оптимизацией. "Важным" компилятором является компилятор JIT, который находится внутри JVM.
В вашем примере, в наиболее общем случае, myList.size() представляет собой простой диспетчер метода, который возвращает содержимое поля в экземпляре List. Это незначительная работа по сравнению с тем, что подразумевается под System.out.println("Hello") (по крайней мере, один системный вызов, следовательно, сотни тактов по сравнению с не более чем дюжиной для отправки метода). Я очень сомневаюсь, что ваш код может показать значительную разницу в скорости.
В более общем плане JIT-компилятор должен распознавать этот вызов size() как вызов известного экземпляра, чтобы он мог выполнять отправку метода с прямым вызовом функции (что быстрее) или даже встраивать вызов метода size(), сокращение вызова до простого доступа к полю экземпляра.
Ответ 5 · рейтинг 2 · sidereal
Он не может оптимизировать его, потому что mylist.size() может измениться во время выполнения цикла. Даже если он окончательный, это просто означает, что ссылка является окончательной (это означает, что вы не можете переназначить myList какому-либо другому объекту), но методы в myList, такие как remove() и add(), по-прежнему доступны. Final не делает объект неизменным.
Ответ 6 · рейтинг 1 · Jonathan Czitkovics
Второй должен быть быстрее, потому что .size() не нужно вызывать каждый раз при выполнении цикла. Гораздо быстрее сказать 1+2=3 один раз, чем много раз.
Ответ 7 · рейтинг 1 · wsorenson
Имеет смысл, что вторая реализация быстрее, потому что вы сохраняете единственную, окончательную, локальную копию переменной. Компилятор должен был бы выяснить, что размер не может измениться внутри цикла, чтобы производительность была примерно эквивалентной.
Один вопрос: действительно ли такая микрооптимизация имеет значение? Если это так, используйте то, что работает быстрее в ваших тестах и не полагается на оптимизацию компилятора.
Ответ 8 · рейтинг 1 · Tom Hawtin - tackline
Почти наверняка то, что вы видите здесь, - это разница в инлайнинге HotSpot. С более простым циклом он, скорее всего, встроится и, следовательно, избавится от всего лишнего мусора. Он может сделать то же самое встраивание, но сделать это раньше или с меньшими усилиями. Как правило, с микротестами Java вы должны запускать код несколько раз, из чего вы можете определить время запуска, среднее время и отклонения.
Ответ 9 · рейтинг 1 · fastcodejava
Компилятор Java оптимизировал бы это так, но не сделал этого, увидев забавное условие. Если бы вы так написали, то проблем бы не было.
for (int i = myList.size(); i < 1000000; i--) {
System.out.println("Hello");
}
Ответ 10 · Pindatjuh
В случаях "оптимизации компилятора" лучшее, что вы можете сделать, это циклы for-each:
for(final String x : myList) { ... }
Что позволяет компилятору обеспечить самую быструю реализацию.
Редактировать:
Разница между вашими примерами кода заключается во втором аргументе цикла for. В первом примере виртуальная машина будет выполнять вызов метода (более дорогой) и, следовательно, медленнее (значительно только при большом количестве итераций). Во втором примере виртуальная машина выполнит всплывающее окно стека (менее дорого, а локальные переменные находятся в стеке) и, следовательно, быстрее (значительно только при большом количестве итераций: только для одной итерации первая быстрее, с точки зрения использования памяти).
Также: "Преждевременная оптимизация - корень всех зол". Знаменитый закон Дональда Кнута.
Ответ 11 · Jonathan M Davis
Как всегда с такими вещами, вам придется запустить их оба, чтобы увидеть, что быстрее, учитывая используемую вами реализацию. Тем не менее, первый имеет потенциальное снижение производительности из-за необходимости вызывать size() на каждой итерации, а вызов функции обходится дороже, чем простая проверка переменной напрямую. Однако возможно, что этот вызов функции может быть оптимизирован в зависимости от вашего кода и того, что делает компилятор, поэтому вам придется запустить тесты, чтобы увидеть.
Однако, как было указано Pindatjuh, лучше использовать цикл foreach, когда вы собираетесь перебирать всю коллекцию таким образом. Это должно позволить компилятору лучше оптимизировать вещи и менее подвержено ошибкам.
Ответ 12 · pajton
Разница в том, что для каждой итерации на один вызов метода меньше, поэтому вторая версия должна работать немного быстрее. Хотя, если вы используете компилятор Just-In-Time, он может оптимизировать это, выясняя, что он не меняется во время цикла. Стандартная реализация Java включает JIT, но не каждая реализация Java.