Я новичок в Java 8. Я до сих пор не знаю API глубоко, но я сделал небольшой неформальный тест, чтобы сравнить производительность нового Streams API и старых добрых коллекций.
Тест состоит в фильтрации списка 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)
Для этого конкретного теста потоки примерно вдвое медленнее, чем коллекции, и параллелизм не помогает (или я использую его неправильно?).
Вопросы:
- Это честный тест? Я сделал какую-нибудь ошибку?
- Стримы медленнее коллекций? Кто-нибудь сделал хороший формальный тест по этому поводу?
- К какому подходу я должен стремиться?
Обновленные результаты.
Я провел тест 1k раз после разогрева JVM (1k итераций), как посоветовал @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)
В этом случае потоки более производительны. Интересно, что будет наблюдаться в приложении, в котором функция фильтрации вызывается только один или два раза во время выполнения.
источник
IntStream
вместо этого?toList
должно выполняться параллельно, даже если оно собирается в список, не защищенный потоками, поскольку различные потоки перед объединением будут собираться в ограниченные потоками промежуточные списки.Ответы:
Прекратите использовать
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()
звонков). Потоки больше связаны с удобством и безопасностью кодирования. Удобство - здесь работает компромисс скорости.источник
@Benchmark
вместо@GenerateMicroBenchmark
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)
без edit (
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())
команда. Сбор в одну коллекцию по существу создает узкое место в производительности и накладные расходы в случае одновременного выполнения. Относительную стоимость накладных расходов можно оценить, заменивДля потоков это можно сделать с помощью
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()
.источник
collect(Collectors.toList())
команда, т.е. может возникнуть ситуация, когда вам нужно адресовать одну коллекцию несколькими потоками». Я почти уверен, что онаtoList
собирает в несколько разных экземпляров списка параллельно. Только на последнем шаге в коллекции элементы переносятся в один список, а затем возвращаются. Так что накладных расходов на синхронизацию быть не должно. Вот почему у коллекционеров есть функции поставщика, накопителя и объединителя. (Конечно, это может быть медленным по другим причинам.)collect
реализации здесь. Но в конечном итоге несколько списков нужно объединить в один, и похоже, что объединение - самая тяжелая операция в данном примере.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); }
Я немного изменил код, запустил на своем Mac Book Pro с 8 ядрами, получил разумный результат:
Collections: Elapsed time: 1522036826 ns (1.522037 seconds) Streams: Elapsed time: 4315833719 ns (4.315834 seconds) Parallel streams: Elapsed time: 261152901 ns (0.261153 seconds)
источник
Для того, что вы пытаетесь сделать, я бы все равно не использовал обычные java api. Идет масса операций по упаковке / распаковке, поэтому накладные расходы на производительность огромны.
Лично я считаю, что многие разработанные API - это дерьмо, потому что они создают много мусора для объектов.
Попробуйте использовать примитивные массивы double / int и попробуйте сделать это однопоточным и посмотрите, какова производительность.
PS: Возможно, вы захотите взглянуть на JMH, чтобы выполнить тест. Он устраняет некоторые типичные ошибки, такие как разогрев JVM.
источник