Вопрос производительности 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));
}

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

13 ответов


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

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

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

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

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 НС, тогда вы должны заботиться об этом. Иначе не надо.


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

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

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


однажды я работал над проектом, где моей первой задачей было отследить какой-то безумно медленный код (он был на совершенно новой машине 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 ()", вероятно, возвращает переменную, и как таким образом, VM встроит его. На ВМ как на Android это, наверное, не. Поэтому ответ - "это зависит".

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


отметим, что javac компилятор имеет ничего общего с оптимизацией. "Важным" компилятором является JIT-компилятор, который живет в JVM.

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

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


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


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


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

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


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


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

for (int i = myList.size(); i < 1000000; i--) {
    System.out.println("Hello");
}

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

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

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

изменить:

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

также: "преждевременная оптимизация-это корень всех зол.- Печально известный закон Дональда Кнута.


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

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


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


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

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

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