Статьи

Производительность Java: для каждого против потоковой передачи

Является ли подсчет вверх или вниз в цикле for наиболее эффективным способом итерации? Иногда ответ ни один. Прочитайте этот пост и поймите влияние различных вариантов итерации.

Производительность итерации

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

01
02
03
04
05
06
07
08
09
10
private static final int ITERATIONS = 10_000;
 
@Benchmark
public int forUp() {
    int sum = 0;
    for (int i = 0; i < ITERATIONS; i++) {
        sum += i;
    }
    return sum;
}

Иногда мы сталкиваемся с циклом for, который начинается с заранее определенного неотрицательного значения, а затем начинает обратный отсчет. Это довольно часто встречается в самом JDK, например, в классе String . Вот пример решения предыдущей задачи путем обратного отсчета вместо повышения.

1
2
3
4
5
6
7
8
@Benchmark
public int forDown() {
    int sum = 0;
    for (int i = ITERATIONS; i-- > 0😉 {
        sum += i;
    }
    return sum;
}

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

Еще один способ сделать то же самое, используя
IntStream выглядит так:

1
2
3
4
5
@Benchmark
public int stream() {
    return IntStream.range(0, ITERATIONS)
        .sum();
}

Если для больших итераций требуется больше производительности, относительно легко сделать поток параллельным, просто добавив в .parallel() оператор .parallel() . Это не рассматривается в этой статье.

Выступление под Graal VM

Выполнение этих тестов под GraalVM (rc-11, с новым компилятором C2, который поставляется с GraallVM) на моем ноутбуке (MacBook Pro, середина 2015 года, 2,2 ГГц Intel Core i7) дает следующее:

1
2
3
4
Benchmark              Mode  Cnt       Score       Error  Units
ForBenchmark.forDown  thrpt    5  311419.166 ±  4201.724  ops/s
ForBenchmark.forUp    thrpt    5  309598.916 ± 12998.579  ops/s
ForBenchmark.stream   thrpt    5  312360.089 ±  8291.792  ops/s

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

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

Умная математика

Из математики мы напоминаем, что сумма последовательных чисел, начинающихся с нуля, равна N * (N + 1) / 2, где N — наибольшее число в ряду. Запуск этого теста:

1
2
3
4
@Benchmark
public int math() {
    return ITERATIONS * (ITERATIONS + 1) / 2;
}

дает нам увеличение производительности более чем в 1000 раз по сравнению с предыдущими реализациями:

1
2
Benchmark           Mode  Cnt          Score          Error  Units
ForBenchmark.math  thrpt    5  395561077.984 ± 11138012.141  ops/s

Чем больше итераций, тем больше выигрыш. Хитрость иногда превосходит грубую силу.

Сверхбыстрые потоки данных

С Speedment HyperStream можно получить аналогичную производительность с данными из баз данных. Узнайте больше здесь на HyperStream.

Выводы

На некоторых обычно используемых аппаратных средствах / JVM не имеет значения, будем ли мы выполнять итерацию вверх или вниз в наших циклах for. Более современные JVM способны оптимизировать потоковые итерации, поэтому они имеют эквивалентную или даже лучшую производительность, чем циклы for.

На мой взгляд, потоковый код, как правило, более читабелен по сравнению с циклами for, и поэтому я считаю, что в будущем потоки, вероятно, станут фактическим приспособлением для итераций.

Содержимое базы данных может передаваться с высокой производительностью с помощью Speedment HyperStream.

Смотрите оригинальную статью здесь: Производительность Java: For-eaching против Streaming

Мнения, высказанные участниками Java Code Geeks, являются их собственными.