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

по сравнению с

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 этот код? Я мог думать, что он будет, если условие цикла будет неизменным объектом (например, Массив строк).

21
задан Tom Hawtin - tackline 4 March 2010 в 23:47
поделиться

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

25
ответ дан 29 November 2019 в 06:49
поделиться

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

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

9
ответ дан 29 November 2019 в 06:49
поделиться

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

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

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

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

10
ответ дан 29 November 2019 в 06:49
поделиться

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

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

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

0
ответ дан 29 November 2019 в 06:49
поделиться

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

for (int i = myList.size(); i < 1000000; i--) {
    System.out.println("Hello");
}
1
ответ дан 29 November 2019 в 06:49
поделиться

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

1
ответ дан 29 November 2019 в 06:49
поделиться

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

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

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

5
ответ дан 29 November 2019 в 06:49
поделиться

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

0
ответ дан 29 November 2019 в 06:49
поделиться

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

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

0
ответ дан 29 November 2019 в 06:49
поделиться

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

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

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

Изменить:

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

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

0
ответ дан 29 November 2019 в 06:49
поделиться

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

1
ответ дан 29 November 2019 в 06:49
поделиться

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

2
ответ дан 29 November 2019 в 06:49
поделиться

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

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

1
ответ дан 29 November 2019 в 06:49
поделиться
Другие вопросы по тегам:

Похожие вопросы: