рассмотрение этого примера:
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));
}
по сравнению с
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));
}
Это будет иметь какое-либо значение? На моей машине вторая, кажется, работает быстрее, но я не знаю, действительно ли это точно. Будет компилятор optimze этот код? Я мог думать, что он будет, если условие цикла будет неизменным объектом (например, Массив строк).
Если вы хотите протестировать что-то подобное, вы действительно должны оптимизировать свой микробенчмарк, чтобы измерить то, что вас волнует.
Во-первых, сделайте цикл недорогим , но его невозможно пропустить. Обычно удается вычислить сумму.
Во-вторых, сравните два тайминга.
Вот код, который выполняет и то и другое:
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 нс, вам следует об этом позаботиться. В противном случае не делайте этого.
Однажды я работал над проектом, в котором моей первой задачей было отследить какой-то безумно медленный код (он был на совершенно новой машине 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, вероятно, нет. Итак, ответ - «это зависит от обстоятельств».
Мое личное предпочтение - никогда не вызывать метод более одного раза, если предполагается, что он каждый раз возвращает один и тот же результат. Таким образом, если метод включает в себя вычисление, оно выполняется только один раз, и тогда это никогда не будет проблемой.
Лично я не думаю, что из такого надуманного примера можно сделать какие-либо значимые выводы.
Но если вы действительно хотите знать, почему бы не использовать javap для декомпиляции кода и посмотреть, что изменилось? Зачем гадать, что делает компилятор, если вы можете сами убедиться, не спрашивая здесь?
Байт-код для первого случая:
public class Stackoverflow extends java.lang.Object{
public Stackoverflow();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_4
1: anewarray #2; //class java/lang/String
4: dup
5: iconst_0
6: ldc #3; //String A
8: aastore
9: dup
10: iconst_1
11: ldc #4; //String B
13: aastore
14: dup
15: iconst_2
16: ldc #5; //String C
18: aastore
19: dup
20: iconst_3
21: ldc #6; //String D
23: aastore
24: invokestatic #7; //Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List
27: astore_1
28: invokestatic #8; //Method java/lang/System.currentTimeMillis:()J
31: lstore_2
32: ldc #9; //int 1000000
34: istore 4
36: iload 4
38: aload_1
39: invokeinterface #10, 1; //InterfaceMethod java/util/List.size:()I
44: if_icmple 61
47: getstatic #11; //Field java/lang/System.out:Ljava/io/PrintStream;
50: ldc #12; //String Hello
52: invokevirtual #13; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
55: iinc 4, -1
58: goto 36
61: invokestatic #8; //Method java/lang/System.currentTimeMillis:()J
64: lstore 4
66: getstatic #11; //Field java/lang/System.out:Ljava/io/PrintStream;
69: new #14; //class java/lang/StringBuilder
72: dup
73: invokespecial #15; //Method java/lang/StringBuilder."<init>":()V
76: ldc #16; //String Finish:
78: invokevirtual #17; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/la
81: lload 4
83: lload_2
84: lsub
85: invokevirtual #18; //Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
88: invokevirtual #19; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
91: invokevirtual #13; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
94: return
}
Байт-код для второго случая:
public class Stackoverflow extends java.lang.Object{
public Stackoverflow();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_4
1: anewarray #2; //class java/lang/String
4: dup
5: iconst_0
6: ldc #3; //String A
8: aastore
9: dup
10: iconst_1
11: ldc #4; //String B
13: aastore
14: dup
15: iconst_2
16: ldc #5; //String C
18: aastore
19: dup
20: iconst_3
21: ldc #6; //String D
23: aastore
24: invokestatic #7; //Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
27: astore_1
28: invokestatic #8; //Method java/lang/System.currentTimeMillis:()J
31: lstore_2
32: aload_1
33: invokeinterface #9, 1; //InterfaceMethod java/util/List.size:()I
38: istore 4
40: ldc #10; //int 1000000
42: istore 5
44: iload 5
46: iload 4
48: if_icmple 65
51: getstatic #11; //Field java/lang/System.out:Ljava/io/PrintStream;
54: ldc #12; //String Hello
56: invokevirtual #13; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
59: iinc 5, -1
62: goto 44
65: invokestatic #8; //Method java/lang/System.currentTimeMillis:()J
68: lstore 5
70: getstatic #11; //Field java/lang/System.out:Ljava/io/PrintStream;
73: new #14; //class java/lang/StringBuilder
76: dup
77: invokespecial #15; //Method java/lang/StringBuilder."<init>":()V
80: ldc #16; //String Finish:
82: invokevirtual #17; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
85: lload 5
87: lload_2
88: lsub
89: invokevirtual #18; //Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
92: invokevirtual #19; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
95: invokevirtual #13; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
98: return
}
Есть различия, но я не уверен Я могу однозначно сказать об их влиянии на производительность.
Я бы закодировал второй, потому что это означало бы (на первый взгляд) один вызов метода, а не один вызов на каждую итерацию цикла. Я не знаю, сможет ли компилятор оптимизировать это, но уверен, что смогу сделать это довольно легко. Я так и делаю, независимо от того, как он влияет на время стены.
В последнем примере вам не нужно разрешать текущий размер массива, поэтому он будет немного быстрее, чем в первом примере.
Просто помните, что это полезно только в том случае, если вы не меняете количество значений в вашем массиве.
В Android рекомендуется использовать последний пример из этого примера, Designing for Performance. http://developer.android.com/guide/practices/design/performance.html#foreach
Компилятор Java мог бы оптимизировать это так, но не сделал этого, увидев забавное условие. Если бы вы написали это так, не было бы проблем.
for (int i = myList.size(); i < 1000000; i--) {
System.out.println("Hello");
}
Второй должен быть быстрее, потому что .size ()
не нужно вызывать каждый раз при выполнении цикла. Гораздо быстрее сказать 1 + 2 = 3 один раз, чем много раз.
Обратите внимание, что компилятор javac
не имеет ничего общего с оптимизацией. «Важным» компилятором является JIT-компилятор, который находится внутри JVM.
В вашем примере в наиболее общем случае myList.size ()
- это простой метод диспетчеризации, который возвращает содержимое поля в экземпляре List
. Это незначительная работа по сравнению с тем, что подразумевается в System.out.println ("Hello")
(по крайней мере, один системный вызов, следовательно, сотни тактовых циклов, по сравнению с не более чем дюжиной для диспетчеризации метода ).Я очень сомневаюсь, что ваш код может показать значительную разницу в скорости.
В более общем плане JIT-компилятор должен распознавать этот вызов size ()
как вызов известного экземпляра, чтобы он мог выполнить отправку метода с помощью прямого вызова функции ( быстрее) или даже встроить вызов метода size ()
, сократив вызов до простого доступа к полю экземпляра.
Разница в том, что для каждой итерации на один вызов метода меньше, поэтому вторая версия должна работать немного быстрее. Хотя, если вы используете компилятор Just-In-Time, он может оптимизировать это - выясняя, что он не меняется во время цикла. Стандартная реализация Java поддерживает JIT, но не каждая реализация Java.
Как всегда в таких случаях, вам придется выполнить оба варианта, чтобы увидеть, какой из них быстрее, учитывая используемую вами реализацию. Однако, первый вариант имеет потенциальный штраф за производительность, поскольку приходится вызывать size() каждую итерацию, а вызов функции обходится дороже, чем простая проверка переменной напрямую. Однако возможно, что этот вызов функции может быть оптимизирован в зависимости от вашего кода и действий компилятора, так что вам придется провести тесты, чтобы проверить это.
Однако, как отметил Pindatjuh, лучше использовать цикл foreach, когда вы собираетесь итерировать всю коллекцию подобным образом. Это позволит компилятору лучше оптимизировать работу и будет менее подвержен ошибкам.
В случаях «оптимизации компилятора» лучшее, что вы можете сделать, - это циклы для каждого:
for(final String x : myList) { ... }
Что позволяет компилятору обеспечить самую быструю реализацию.
Изменить:
Разница между вашими примерами кода заключается во втором аргументе цикла for. В первом примере виртуальная машина выполняет вызов метода (более затратный) и, следовательно, медленнее (имеет значение только при большом количестве итераций). Во втором примере виртуальная машина будет выталкивать стек (менее затратный, а локальные переменные находятся в стеке) и, следовательно, быстрее (имеет значение только при большом количестве итераций: только для одной итерации первая выполняется быстрее, с точки зрения использования памяти).
Также: «Преждевременная оптимизация - корень всех зол». Печально известный закон Дональда Кнута.
Почти наверняка вы видите здесь разницу во встраивании HotSpot. С более простым циклом он с большей вероятностью будет встроен и, следовательно, избавится от всего лишнего мусора. Он может делать то же самое встраивание, но делать это раньше или с меньшими усилиями. Как правило, с помощью микробенчмарков Java вы должны запускать код несколько раз, из которых вы можете определить время запуска, среднее время и отклонения.
Он не может оптимизировать его, потому что mylist.size() может измениться во время выполнения цикла. Даже если он конечный, это означает, что ссылка на него конечна (то есть вы не можете переназначить myList на другой объект), но методы myList, такие как remove() и add(), все еще доступны. Final не делает объект неизменяемым.
Логично, что вторая реализация быстрее, потому что вы храните единственную, конечную, локальную копию переменной. Компилятор должен понять, что размер не может меняться внутри цикла, чтобы производительность была примерно эквивалентной.
Возникает вопрос: действительно ли такая микрооптимизация имеет значение? Если да, то выбирайте то, что работает быстрее в ваших тестах и не зависит от оптимизации компилятора.