Подтвердить что ты не робот

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

Я только что прочитал о Branch-Prediction и хотел попробовать, как это работает с потоками Java 8.

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

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

Выход:

  • Для Sorted-Array:

    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) 
    
  • Для Un-Sorted Array:

    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]

Выход:

  • Для Sorted-List:

    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) 
    
  • Для Un-Sorted List

    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) 
    

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

  • Я согласен с тем, что программирование с потоками приятно и доступно для некоторых сценариев, но когда мы теряем производительность, зачем нам их использовать? Я что-то упускаю?
  • Каков сценарий, в котором потоки выполняют равные циклы? Это только в том случае, когда ваша функция определена, занимает много времени, что приводит к незначительной производительности цикла?
  • Ни в одном из сценариев я не видел, чтобы потоки использовали ветвь-предсказание (я пробовал с отсортированными и неупорядоченными потоками, но не использовал их. Это дало более чем в два раза больше влияния производительности по сравнению с обычным потоки)?
4b9b3361

Ответ 1

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

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

Есть ли что-то, что я упускаю?

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

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

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

Ни в одном из сценариев я не видел потоков, использующих предсказание ветвей

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

Ответ 2

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

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

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

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

Ответ 3

Все сказано, но я хочу показать вам, как выглядит ваш код с помощью 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, и, возможно, также имеет отношение к ветвям. Теперь вам решать интерпретировать результаты тестов.

Ответ 4

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

Я только что прочитал о Branch-Prediction и хотел попробовать, как это работает с Java 8 Streams

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

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

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

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

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

Ответ 5

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

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

  • Многопоточность
  • JIT

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

Да!

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

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

Посмотрим jdk/src/share/vm/runtime/globals.hpp

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

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

Как насчет прогноза ветвления?

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

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

Никогда.

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

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