Когда следует предпочесть потоки традиционным циклам для лучшей производительности? Используют ли потоки преимущества прогнозирования ветвей?

Я только что прочитал о Филиала-Прогнозирование и хотел попробовать, как это работает с Java 8 потоков.

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

int totalSize = 32768;
int filterValue = 1280;
int[] array = new int[totalSize];
Random rnd = new Random(0);
int loopCount = 10000;

for (int i = 0; i < totalSize; i++) {
    // array[i] = rnd.nextInt() % 2560; // Unsorted Data
    array[i] = i; // Sorted Data
}

long start = System.nanoTime();
long sum = 0;
for (int j = 0; j < loopCount; j++) {
    for (int c = 0; c < totalSize; ++c) {
        sum += array[c] >= filterValue ? array[c] : 0;
    }
}
long total = System.nanoTime() - start;
System.out.printf("Conditional Operator Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9));

start = System.nanoTime();
sum = 0;
for (int j = 0; j < loopCount; j++) {
    for (int c = 0; c < totalSize; ++c) {
        if (array[c] >= filterValue) {
            sum += array[c];
        }
    }
}
total = System.nanoTime() - start;
System.out.printf("Branch Statement Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9));

start = System.nanoTime();
sum = 0;
for (int j = 0; j < loopCount; j++) {
    sum += Arrays.stream(array).filter(value -> value >= filterValue).sum();
}
total = System.nanoTime() - start;
System.out.printf("Streams Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9));

start = System.nanoTime();
sum = 0;
for (int j = 0; j < loopCount; j++) {
    sum += Arrays.stream(array).parallel().filter(value -> value >= filterValue).sum();
}
total = System.nanoTime() - start;
System.out.printf("Parallel Streams Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9));

выход :

  1. Для Отсортированного Массива :

    Conditional Operator Time : 294062652 ns, (0.294063 sec) 
    Branch Statement Time : 272992442 ns, (0.272992 sec) 
    Streams Time : 806579913 ns, (0.806580 sec) 
    Parallel Streams Time : 2316150852 ns, (2.316151 sec) 
    
  2. Для Не Отсортированного Массива:

    Conditional Operator Time : 367304250 ns, (0.367304 sec) 
    Branch Statement Time : 906073542 ns, (0.906074 sec) 
    Streams Time : 1268648265 ns, (1.268648 sec) 
    Parallel Streams Time : 2420482313 ns, (2.420482 sec) 
    

я попробовал тот же код с использованием список:
list.stream() вместо Arrays.stream(array)
list.get(c) вместо array[c]

выход :

  1. Для Отсортированного Списка :

    Conditional Operator Time : 860514446 ns, (0.860514 sec) 
    Branch Statement Time : 663458668 ns, (0.663459 sec) 
    Streams Time : 2085657481 ns, (2.085657 sec) 
    Parallel Streams Time : 5026680680 ns, (5.026681 sec) 
    
  2. Для Не Отсортированного Списка

    Conditional Operator Time : 704120976 ns, (0.704121 sec) 
    Branch Statement Time : 1327838248 ns, (1.327838 sec) 
    Streams Time : 1857880764 ns, (1.857881 sec) 
    Parallel Streams Time : 2504468688 ns, (2.504469 sec) 
    

я упомянул несколько блогов этой & этой которые предполагают ту же проблему производительности w.r.t потоки.

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

5 ответов


Я согласен с тем, что программирование с потоками приятно и проще для некоторых сценариев, но когда мы теряем производительность, зачем нам их использовать?

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

есть что-то, что я упускаю?

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

какой сценарий, в котором потоки выполняют равные циклам? Это только в том случае, когда ваша определенная функция занимает много времени, что приводит к незначительной производительности цикла?

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

In ни один из сценариев я не мог видеть потоки, использующие преимущество ветвления-предсказание

прогнозирование ветвей-это функция ЦП, а не функция JVM или streams.


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

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

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

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


все сказано, но я хочу показать вам, как ваш код должен выглядеть с помощью JMH.

@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@Measurement(iterations = 10, timeUnit = TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Threads(1)
@Warmup(iterations = 5, timeUnit = TimeUnit.NANOSECONDS)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {

  private final int totalSize = 32_768;
  private final int filterValue = 1_280;
  private final int loopCount = 10_000;
  // private Random rnd;

  private int[] array;

  @Setup
  public void setup() {
    array = IntStream.range(0, totalSize).toArray();

    // rnd = new Random(0);
    // array = rnd.ints(totalSize).map(i -> i % 2560).toArray();
  }

  @Benchmark
  public long conditionalOperatorTime() {
    long sum = 0;
    for (int j = 0; j < loopCount; j++) {
      for (int c = 0; c < totalSize; ++c) {
        sum += array[c] >= filterValue ? array[c] : 0;
      }
    }
    return sum;
  }

  @Benchmark
  public long branchStatementTime() {
    long sum = 0;
    for (int j = 0; j < loopCount; j++) {
      for (int c = 0; c < totalSize; ++c) {
        if (array[c] >= filterValue) {
          sum += array[c];
        }
      }
    }
    return sum;
  }

  @Benchmark
  public long streamsTime() {
    long sum = 0;
    for (int j = 0; j < loopCount; j++) {
      sum += IntStream.of(array).filter(value -> value >= filterValue).sum();
    }
    return sum;
  }

  @Benchmark
  public long parallelStreamsTime() {
    long sum = 0;
    for (int j = 0; j < loopCount; j++) {
      sum += IntStream.of(array).parallel().filter(value -> value >= filterValue).sum();
    }
    return sum;
  }
}

результаты для отсортированного массива:

Benchmark                            Mode  Cnt           Score           Error  Units
MyBenchmark.branchStatementTime      avgt   30   119833793,881 ±   1345228,723  ns/op
MyBenchmark.conditionalOperatorTime  avgt   30   118146194,368 ±   1748693,962  ns/op
MyBenchmark.parallelStreamsTime      avgt   30   499436897,422 ±   7344346,333  ns/op
MyBenchmark.streamsTime              avgt   30  1126768177,407 ± 198712604,716  ns/op

результаты для несортированных данных:

Benchmark                            Mode  Cnt           Score           Error  Units
MyBenchmark.branchStatementTime      avgt   30   534932594,083 ±   3622551,550  ns/op
MyBenchmark.conditionalOperatorTime  avgt   30   530641033,317 ±   8849037,036  ns/op
MyBenchmark.parallelStreamsTime      avgt   30   489184423,406 ±   5716369,132  ns/op
MyBenchmark.streamsTime              avgt   30  1232020250,900 ± 185772971,366  ns/op

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


Я добавлю свой 0.02$ здесь.

Я только что прочитал о предсказании ветвей и хотел попробовать, как это работает с Java 8 Streams

прогнозирование ветвей-это функция процессора, она не имеет ничего общего с JVM. Это необходимо, чтобы держать CPU конвейер полным и готов сделать что-то. Измерение или предсказания предсказание ветви чрезвычайно сложно (если вы на самом деле не знаете, что именно будет делать процессор). Это будет зависеть, по крайней мере, от нагрузки что процессор имеет прямо сейчас (это может быть намного больше, чем только ваша программа).

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

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

одновременно работает С Spring / Hibernate / Services и т. д., которые делают вещи в миллисекундах и ваших потоках в нано-секундах, и вы беспокоитесь о производительности? Вы сомневаетесь в скорости вашей самой быстрой части кода? Это конечно теоретическая вещь.

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


как моя программа Java может работать быстро?

короче говоря, Java-программы могут быть ускорены:

  1. многопоточность
  2. JIT

связаны ли потоки с ускорением программы Java?

да!

  1. Примечание Collection.parallelStream() и Stream.parallel() методы многопоточность
  2. можно писать for цикл, который достаточно длинный для JIT, чтобы пропустить. Лямбда-выражений обычно маленький и может быть скомпилирован JIT => есть возможность получить производительность

какой поток сценариев может быть быстрее, чем for петли?

давайте посмотрим на jdk/src/share/vm/runtime / globals.ГЭС

develop(intx, HugeMethodLimit,  8000,
        "Don't compile methods larger than this if "
        "+DontCompileHugeMethods")

если у вас достаточно длинный цикл, он не будет скомпилирован JIT и будет работать медленно. Если вы переписываете такой цикл в поток, вы, вероятно, будете использовать map, filter, flatMap методы разделения кода части и каждая часть могут быть достаточно малы для приспособления под предел. Конечно, написание огромных методов имеет и другие недостатки, кроме компиляции JIT. Этот сценарий можно рассмотреть, если, например, у вас есть много сгенерированного кода.

что о предсказании ветви?

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

Итак, когда Я переписываю свои циклы в потоки, чтобы достичь лучшей производительности?

никогда.

преждевременная оптимизация-корень всех зол ©Дональд Кнут

попробуйте вместо этого оптимизировать алгоритм. Потоки интерфейс для функционального программирования, а не инструмент для ускорения циклов.