Java 8: производительность потоков и коллекций
Я новичок в Java 8. Я до сих пор не знаю API, но я сделал небольшой неофициальный тест для сравнения производительности нового API Streams и старых добрых коллекций.
Тест состоит в фильтрации списка Integer
чисел и для каждого четного числа вычисляет квадратный корень и сохраняет его в результирующем List
Double
чисел.
Вот код:
public static void main(String[] args) {
//Calculating square root of even numbers from 1 to N
int min = 1;
int max = 1000000;
List<Integer> sourceList = new ArrayList<>();
for (int i = min; i < max; i++) {
sourceList.add(i);
}
List<Double> result = new LinkedList<>();
//Collections approach
long t0 = System.nanoTime();
long elapsed = 0;
for (Integer i : sourceList) {
if(i % 2 == 0){
result.add(Math.sqrt(i));
}
}
elapsed = System.nanoTime() - t0;
System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Stream approach
Stream<Integer> stream = sourceList.stream();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Parallel stream approach
stream = sourceList.stream().parallel();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
}.
И вот результаты для двухъядерного компьютера:
Collections: Elapsed time: 94338247 ns (0,094338 seconds)
Streams: Elapsed time: 201112924 ns (0,201113 seconds)
Parallel streams: Elapsed time: 357243629 ns (0,357244 seconds)
Для этого конкретного теста потоки примерно в два раза медленнее, чем коллекции, и параллелизм не помогает (или я использую его неправильно?).
Вопросы:
- Справедлив ли этот тест? Я сделал какую-нибудь ошибку?
- Потоки медленнее коллекций? Кто-нибудь сделал хороший формальный тест на это?
- К какому подходу я должен стремиться?
Обновленные результаты.
Я выполнил тест 1к раз после прогрева JVM (1к итераций), как советовал @pveentjer:
Collections: Average time: 206884437,000000 ns (0,206884 seconds)
Streams: Average time: 98366725,000000 ns (0,098367 seconds)
Parallel streams: Average time: 167703705,000000 ns (0,167704 seconds)
В этом случае потоки более производительны. Интересно, что будет наблюдаться в приложении, где функция фильтрации вызывается только один или два раза во время выполнения.
Ответы
Ответ 1
-
Прекратить использование LinkedList
для чего угодно, кроме тяжелого удаления из середины списка с помощью итератора.
-
Остановите запись кода бенчмаркинга вручную, используйте JMH.
Правильные тесты:
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(StreamVsVanilla.N)
public class StreamVsVanilla {
public static final int N = 10000;
static List<Integer> sourceList = new ArrayList<>();
static {
for (int i = 0; i < N; i++) {
sourceList.add(i);
}
}
@Benchmark
public List<Double> vanilla() {
List<Double> result = new ArrayList<>(sourceList.size() / 2 + 1);
for (Integer i : sourceList) {
if (i % 2 == 0){
result.add(Math.sqrt(i));
}
}
return result;
}
@Benchmark
public List<Double> stream() {
return sourceList.stream()
.filter(i -> i % 2 == 0)
.map(Math::sqrt)
.collect(Collectors.toCollection(
() -> new ArrayList<>(sourceList.size() / 2 + 1)));
}
}
Результат:
Benchmark Mode Samples Mean Mean error Units
StreamVsVanilla.stream avgt 10 17.588 0.230 ns/op
StreamVsVanilla.vanilla avgt 10 10.796 0.063 ns/op
Так же, как я ожидал, что реализация потока будет довольно медленной. JIT может встраивать все элементы лямбды, но не представляет собой совершенно лаконичный код в виде ванильной версии.
Как правило, потоки Java 8 не являются волшебными. Они не могли ускорить уже хорошо реализованные вещи (возможно, с помощью простых итераций или Java 5 для каждого оператора, замененного вызовами Iterable.forEach()
и Collection.removeIf()
). Потоки - это больше о удобстве и безопасности кодирования. Здесь работает удобство - скорость компромисса.
Ответ 2
1) Вы видите время менее 1 секунды, используя ваш тест. Это означает, что на ваши результаты могут оказать сильное влияние побочные эффекты. Итак, я увеличил вашу задачу в 10 раз
int max = 10_000_000;
и побежал ваш тест. Мои результаты:
Collections: Elapsed time: 8592999350 ns (8.592999 seconds)
Streams: Elapsed time: 2068208058 ns (2.068208 seconds)
Parallel streams: Elapsed time: 7186967071 ns (7.186967 seconds)
без редактирования (int max = 1_000_000
) результаты были
Collections: Elapsed time: 113373057 ns (0.113373 seconds)
Streams: Elapsed time: 135570440 ns (0.135570 seconds)
Parallel streams: Elapsed time: 104091980 ns (0.104092 seconds)
Это как ваши результаты: поток медленнее, чем сбор. Вывод: много времени было потрачено на инициализацию потока/передачу значений.
2) После увеличения поток задач стал быстрее (это нормально), но параллельный поток оставался слишком медленным. Что не так? Примечание: у вас есть команда collect(Collectors.toList())
в вашей команде. Сбор в единую коллекцию существенно снижает производительность и снижает производительность при одновременном выполнении. Можно оценить относительную стоимость накладных расходов, заменив
collecting to collection -> counting the element count
Для потоков это можно сделать с помощью collect(Collectors.counting())
. Я получил результаты:
Collections: Elapsed time: 41856183 ns (0.041856 seconds)
Streams: Elapsed time: 546590322 ns (0.546590 seconds)
Parallel streams: Elapsed time: 1540051478 ns (1.540051 seconds)
Это для большой задачи! (int max = 10000000
) Вывод: сбор предметов для сбора занял большую часть времени. Самая медленная часть - это добавление в список. Кстати, простой ArrayList
используется для Collectors.toList()
.
Ответ 3
public static void main(String[] args) {
//Calculating square root of even numbers from 1 to N
int min = 1;
int max = 10000000;
List<Integer> sourceList = new ArrayList<>();
for (int i = min; i < max; i++) {
sourceList.add(i);
}
List<Double> result = new LinkedList<>();
//Collections approach
long t0 = System.nanoTime();
long elapsed = 0;
for (Integer i : sourceList) {
if(i % 2 == 0){
result.add( doSomeCalculate(i));
}
}
elapsed = System.nanoTime() - t0;
System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Stream approach
Stream<Integer> stream = sourceList.stream();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> doSomeCalculate(i))
.collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Parallel stream approach
stream = sourceList.stream().parallel();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> doSomeCalculate(i))
.collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
}
static double doSomeCalculate(int input) {
for(int i=0; i<100000; i++){
Math.sqrt(i+input);
}
return Math.sqrt(input);
}
Я немного изменил код, побежал на моем macbook pro, который имеет 8 ядер, я получил разумный результат:
Коллекции: Истекшее время: 1522036826 нс (1.522037 секунд)
Потоки: Истекшее время: 4315833719 нс (4.315834 секунды)
Параллельные потоки: Истекшее время: 261152901 нс (0,261153 секунды)
Ответ 4
Для того, что вы пытаетесь сделать, я бы не использовал обычный java api в любом случае. Существует тонна бокса/распаковки, поэтому накладные расходы высоки.
Лично я считаю, что много разработанных API - это дерьмо, потому что они создают много помех для объекта.
Попробуйте использовать примитивные массивы double/int и попытайтесь сделать это однопоточным и посмотреть, что такое производительность.
PS:
Возможно, вам стоит взглянуть на JMH, чтобы позаботиться о выполнении теста. Он заботится о некоторых типичных ловушках, таких как разогревание JVM.