Java для вопроса о производительности цикла

учитывая этот пример:

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));
}

Будет ли это иметь значение? На моей машине второй работает быстрее, но я не знаю, действительно ли он точен. Будет ли компилятор оптимизировать этот код? Я мог подумать, что он будет, если условие цикла является неизменяемым объектом (например, массивом строк).


person kukudas    schedule 04.03.2010    source источник
comment
Если вы хотите измерить прошедшее время (и хотите, чтобы оно было точным), вам следует использовать System.nanoTime() BTW.   -  person Pascal Thivent    schedule 05.03.2010
comment
Запомнит этот хороший момент.   -  person kukudas    schedule 06.03.2010


Ответы (13)


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

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

Во-вторых, сравните два тайминга.

Вот некоторый код, который делает оба:

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 нс, вам следует позаботиться об этом. В противном случае, не делайте этого.

person Rex Kerr    schedule 05.03.2010

Однажды я работал над проектом, где моей первой задачей было отследить какой-то безумно медленный код (это было на совершенно новой машине 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, это, вероятно, не так. Так что ответ "это зависит".

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

person TofuBeer    schedule 04.03.2010
comment
Хорошие моменты, но я хотел бы отметить, что случай strlen немного отличается, потому что на самом деле ему нужно пройтись по данным, чтобы определить его длину. Так что инлайнинг вас не спасет. Вам обязательно нужно сохранить результат в этом случае. Однако, как вы говорите, если вы просто всегда сохраняете значение, оптимизация гарантирована. - person Jonathan M Davis; 05.03.2010
comment
Ошибаетесь, и это java, а длина строки кэшируется в объекте. - person Dean Povey; 05.03.2010
comment
Я знаю, что это Java... в то время, когда код, который я показывал, был написан, Java не была общедоступной. Суть истории заключалась в том, что если метод (или функция) должен возвращать одно и то же значение каждый раз, нет смысла вызывать его снова и снова... и на самом деле это может быть вредно, поскольку может включать дорогостоящие вычисления. каждый раз, когда он вызывается. В этом случае (как я сказал в своем посте!) это, скорее всего, просто возврат переменной (на самом деле я не могу представить случай, когда для этого не будет просто возврата переменной), но даже на некоторых виртуальных машинах (например, та, которую использует Andoid) вызов может быть не встроенным. - person TofuBeer; 05.03.2010

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

Но если вы действительно хотите знать, почему бы не использовать 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
}

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

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

person duffymo    schedule 04.03.2010

Обратите внимание, что компилятор javac не имеет ничего общего с оптимизацией. «Важным» компилятором является компилятор JIT, который находится внутри JVM.

В вашем примере, в наиболее общем случае, myList.size() представляет собой простой диспетчер метода, который возвращает содержимое поля в экземпляре List. Это незначительная работа по сравнению с тем, что подразумевается под System.out.println("Hello") (по крайней мере, один системный вызов, следовательно, сотни тактов по сравнению с не более чем дюжиной для отправки метода). Я очень сомневаюсь, что ваш код может показать значительную разницу в скорости.

В более общем плане JIT-компилятор должен распознавать этот вызов size() как вызов известного экземпляра, чтобы он мог выполнять отправку метода с прямым вызовом функции (что быстрее) или даже встраивать вызов метода size(), сокращение вызова до простого доступа к полю экземпляра.

person Thomas Pornin    schedule 04.03.2010
comment
Согласен с тем, что вызов System.out.println() является самым дорогим. Интересно, какая разница, если бы вы использовали StringBuilder для добавления этих приветствий вместе, а затем распечатывали их с помощью одного вызова System.out.println() вне цикла. - person Tomas Andrle; 05.03.2010
comment
@Thomas Pornin: тем не менее, компилятор javac выполняет некоторую оптимизацию, например удаление вызовов if ( cond ) {...}, когда, скажем, cond является окончательным логическим значением, установленным в false. Так что не совсем верно, что javac не имеет ничего общего с оптимизацией. - person cocotwo; 05.03.2010
comment
@cocotwo: удаление if (false) {...} является частью спецификации Java (код внутри удаленных фигурных скобок не должен приводить к символическим ссылкам на дополнительные классы в созданном файле .class), поэтому я не уверен, что это можно назвать оптимизацией: это не то, что компилятор может или не может делать по своей прихоти. - person Thomas Pornin; 05.03.2010

Он не может оптимизировать его, потому что mylist.size() может измениться во время выполнения цикла. Даже если он окончательный, это просто означает, что ссылка является окончательной (это означает, что вы не можете переназначить myList какому-либо другому объекту), но методы в myList, такие как remove() и add(), по-прежнему доступны. Final не делает объект неизменным.

person sidereal    schedule 04.03.2010
comment
Хорошая точка зрения! Но зависит от ситуации. Приведенные примеры, как ответил duffymo, слишком неоднозначны. - person Pindatjuh; 05.03.2010

Второй должен быть быстрее, потому что .size() не нужно вызывать каждый раз при выполнении цикла. Гораздо быстрее сказать 1+2=3 один раз, чем много раз.

person Jonathan Czitkovics    schedule 04.03.2010
comment
@TofuBeer, как это возможно? Как компилятору узнать, не меняется ли размер списка? Можете ли вы предоставить ссылку? - person Christopher Oezbek; 05.03.2010
comment
Посмотрите на код, его нельзя изменить. Также встраивание не означает этого, это означает замену вызова метода прямым доступом к переменной. - person TofuBeer; 05.03.2010
comment
Размер может измениться, даже если список является окончательным. - person kukudas; 06.03.2010

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

Один вопрос: действительно ли такая микрооптимизация имеет значение? Если это так, используйте то, что работает быстрее в ваших тестах и ​​не полагается на оптимизацию компилятора.

person wsorenson    schedule 04.03.2010

Почти наверняка то, что вы видите здесь, — это разница в инлайнинге HotSpot. С более простым циклом он, скорее всего, встроится и, следовательно, избавится от всего лишнего мусора. Он может сделать то же самое встраивание, но сделать это раньше или с меньшими усилиями. Как правило, с микротестами Java вы должны запускать код несколько раз, из чего вы можете определить время запуска, среднее время и отклонения.

person Tom Hawtin - tackline    schedule 04.03.2010

Компилятор Java оптимизировал бы это так, но не сделал этого, увидев забавное условие. Если бы вы так написали, то проблем бы не было.

for (int i = myList.size(); i < 1000000; i--) {
    System.out.println("Hello");
}
person fastcodejava    schedule 04.03.2010
comment
Вы должны сделать i ›= 0 и i--, я думаю. - person Pindatjuh; 05.03.2010
comment
Я подозреваю, что вы намеревались считать до 0. - person Carl Manaster; 05.03.2010

В случаях "оптимизации компилятора" лучшее, что вы можете сделать, это циклы for-each:

for(final String x : myList) { ... }

Что позволяет компилятору обеспечить самую быструю реализацию.

Редактировать:

Разница между вашими примерами кода заключается во втором аргументе цикла for. В первом примере виртуальная машина будет выполнять вызов метода (более дорогой) и, следовательно, медленнее (значительно только при большом количестве итераций). Во втором примере виртуальная машина выполнит всплывающее окно стека (менее дорого, а локальные переменные находятся в стеке) и, следовательно, быстрее (значительно только при большом количестве итераций: только для одной итерации первая быстрее, с точки зрения использования памяти).

Также: «Преждевременная оптимизация — корень всех зол». Знаменитый закон Дональда Кнута.

person Pindatjuh    schedule 04.03.2010
comment
Однако в этом случае он не перебирает массив. - person Pool; 05.03.2010
comment
Это не имеет значения здесь, потому что OP на самом деле не перебирает коллекцию или массив. - person cletus; 05.03.2010
comment
Но я догадался, что в этом и заключалась цель вопроса; почему еще спрашивающий использовал Array.asList? С другой стороны: синтаксис for-each можно использовать и для массивов. - person Pindatjuh; 05.03.2010
comment
Код, опубликованный Pindatjuh, будет работать в этом примере, поскольку мы используем размер массива как количество циклов. - person Brendan Long; 05.03.2010
comment
На самом деле оператор for-each требует, чтобы компилятор использовал итератор myList (который должен реализовывать интерфейс Iterable). Если итератор работает плохо, компилятор Java ничего не может сделать для оптимизации производительности. - person jarnbjo; 05.03.2010
comment
Это известно, а не печально известно; последний имеет негативный оттенок и используется, когда кто-то известен тем, что делает что-то плохое или нежелательное. Хотя... Кнут сильно вырван из контекста из-за этого и использовался, чтобы отравить мышление целого поколения программистов, так что, возможно, это все-таки печально известная цитата. - person Lawrence Dol; 05.03.2010
comment
+1 за преждевременную оптимизацию — корень всех зол. Сначала сделайте код оптимально читаемым; затем измерьте; тогда только если измерения показывают, что оптимизация слишком медленная. Что вряд ли когда-либо, в опыте большинства программистов в настоящее время. - person Carl Manaster; 05.03.2010

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

Однако, как было указано Pindatjuh, лучше использовать цикл foreach, когда вы собираетесь перебирать всю коллекцию таким образом. Это должно позволить компилятору лучше оптимизировать вещи и менее подвержено ошибкам.

person Jonathan M Davis    schedule 04.03.2010

Разница в том, что для каждой итерации на один вызов метода меньше, поэтому вторая версия должна работать немного быстрее. Хотя, если вы используете компилятор Just-In-Time, он может оптимизировать это, выясняя, что он не меняется во время цикла. Стандартная реализация Java включает JIT, но не каждая реализация Java.

person pajton    schedule 04.03.2010
comment
Стандартная Java использует своевременный компилятор. - person Michael Myers; 05.03.2010
comment
Я скорее имел в виду, что не каждая реализация Java делает это. Отредактировано. - person pajton; 05.03.2010

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

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

В Android рекомендуется использовать последний пример в этом примере Designing for Performance. http://developer.android.com/guide/practices/design/performance.html#foreach

person lars    schedule 04.03.2010