Ужасная производительность Java 8 при использовании вложенных циклов Intstream

Прочитав о Java 8 java.util.stream.Intstream, я заменил некоторые из традиционных циклов потоками. К сожалению, я столкнулся с некоторыми проблемами производительности при работе с вложенными циклами.

Как и ожидалось, следующий код работает примерно на 47 мс на моей машине:

IntStream.range(0, 1000000000).forEach(i -> {});

Тем не менее, вложенность другой IntStream hyper увеличивает время выполнения примерно до 10 458 мс - i.e.:

IntStream.range(0, 1000000000).forEach(i -> {
    IntStream.range(0, 1).forEach(j -> {});
});

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

РЕДАКТИРОВАТЬ: Только для сравнения следующий код работал намного быстрее (в 1801 мс), используя традиционный внутренний цикл. Таким образом, даже принимая во внимание оптимизацию, кажется, что есть больше накладных расходов, используя внутренний IntStream?

final long[] random = {1};
IntStream.range(0, 1000000000).forEach(i -> {
    for (int j = 0; j < 1; j++) {
        random[0] += i;
    }
});

Ответы

Ответ 1

Это не ужасное выступление во втором случае. Это на самом деле невероятно отличная производительность в первом случае. Смотрите, вы перебираете более одного миллиарда элементов, и итерация занимает всего 47 мс. Таким образом, за одну секунду вы сможете перебирать более 1000/47 = 21 миллиард элементов! Частота вашего процессора, вероятно, составляет около 3 ГГц, поэтому вы перебираете 7 элементов в одном цикле процессора! Такая оптимизация выполняется JIT-компилятором для очень простого цикла (на самом деле он абсолютно оптимизирован во время устранения мертвого кода). Однако вы не будете зарабатывать деньги, питая пустые циклы. Если вы добавите хотя бы некоторую нетривиальную логику, некоторые оптимизации будут отключены или станут менее эффективными, поэтому у вас будет значительное снижение производительности.

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

Ответ 2

Из java doc:

void forEach (действие IntConsumer)     Выполняет действие для каждого элемента этого потока.     Это операция терминала.

Операции с терминалом, такие как Stream.forEach или IntStream.sum, могут пересечь поток, чтобы получить результат или побочный эффект. После выполняется операция терминала, рассматривается трубопровод потребляется и больше не может использоваться; если вам нужно пройти то же самое источника данных, вы должны вернуться к источнику данных, чтобы получить новый поток. Практически во всех случаях операции с терминалом очень интересны, завершая их обход источника данных и обработка трубопровода перед возвращением. Только итератор операций терминала() и spliterator() не являются; они предоставляются как "люк-побег" для разрешить произвольные контролируемые клиентами трассировки трубопроводов в случае что существующих операций недостаточно для выполнения задачи.

Есть накладные расходы на создание множества потоков. Вы пытались запустить код с помощью профайлера?