В моей предыдущей статье на эту тему мы узнали, что JDK 8
stream()::count требуется больше времени, чем больше элементов в Stream . Для более новых JDK, таких как Java 11, это больше не относится к простым потоковым конвейерам. Узнайте, как вещи улучшились в самом JDK.
Java 8
В моей предыдущей статье мы могли бы заключить, что операция
list.stream().count() — это O(N) в Java 8, т.е. время выполнения зависит от количества элементов в исходном списке. Прочитать статью
здесь
Java 9 и выше
Как справедливо отметили Николай Парлог (@nipafx) и Брайан Гетц (@BrianGoetz) в Twitter, реализация Stream::count была улучшена, начиная с Java 9. Вот сравнение основных
Код Stream::count между Java 8 и более поздними версиями Java:
Java 8 (из класса ReferencePipeline)
|
1
|
return mapToLong(e -> 1L).sum(); |
Java 9 и более поздние версии (из класса ReduceOps)
|
1
2
3
|
if (StreamOpFlag.SIZED.isKnown(flags)) { return spliterator.getExactSizeIfKnown();} |
|
1
|
... |
Похоже, Stream::count в Java 9 и позже O(1) для Spliterators известного размера, а не O(N) . Давайте проверим эту гипотезу.
Ориентиры
Свойство big-O можно наблюдать, выполнив следующие тесты JMH для Java 8 и Java 11:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
@State(Scope.Benchmark)public class CountBenchmark { private List<Integer> list; @Param({"1", "1000", "1000000"}) private int size; @Setup public void setup() { list = IntStream.range(0, size) .boxed() .collect(toList()); } @Benchmark public long listSize() { return list.size(); } @Benchmark public long listStreamCount() { return list.stream().count(); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(CountBenchmark.class.getSimpleName()) .mode(Mode.Throughput) .threads(Threads.MAX) .forks(1) .warmupIterations(5) .measurementIterations(5) .build(); new Runner(opt).run(); }} |
Это даст следующие результаты на моем ноутбуке (MacBook Pro, середина 2015 года, 2,2 ГГц Intel Core i7):
JDK 8 (из моей предыдущей статьи)
|
1
2
3
4
5
6
7
|
Benchmark (size) Mode Cnt Score Error UnitsCountBenchmark.listSize 1 thrpt 5 966658591.905 ± 175787129.100 ops/sCountBenchmark.listSize 1000 thrpt 5 862173760.015 ± 293958267.033 ops/sCountBenchmark.listSize 1000000 thrpt 5 879607621.737 ± 107212069.065 ops/sCountBenchmark.listStreamCount 1 thrpt 5 39570790.720 ± 3590270.059 ops/sCountBenchmark.listStreamCount 1000 thrpt 5 30383397.354 ± 10194137.917 ops/sCountBenchmark.listStreamCount 1000000 thrpt 5 398.959 ± 170.737 ops/s |
JDK 11
|
1
2
3
4
5
6
7
|
Benchmark (size) Mode Cnt Score Error UnitsCountBenchmark.listSize 1 thrpt 5 898916944.365 ± 235047181.830 ops/sCountBenchmark.listSize 1000 thrpt 5 865080967.750 ± 203793349.257 ops/sCountBenchmark.listSize 1000000 thrpt 5 935820818.641 ± 95756219.869 ops/sCountBenchmark.listStreamCount 1 thrpt 5 95660206.302 ± 27337762.894 ops/sCountBenchmark.listStreamCount 1000 thrpt 5 78899026.467 ± 26299885.209 ops/sCountBenchmark.listStreamCount 1000000 thrpt 5 83223688.534 ± 16119403.504 ops/s |
Как видно, в Java 11 list.stream().count() выполняется list.stream().count()
O(1) а не O(N) .
Брайан Гетц отметил, что некоторые разработчики, которые использовали вызовы метода Stream::peek в Java 8, обнаружили, что эти методы больше не вызывались, если операция терминала Stream::count выполнялась в Java 9 и более поздних версиях. Это вызвало негативную обратную связь с разработчиками JDK. Лично я думаю, что это было правильное решение разработчиков JDK, и это вместо этого предоставило отличную возможность для
Stream::peek получают правильный код.
Более сложные потоковые трубопроводы
В этой главе мы рассмотрим более сложные потоковые конвейеры.
JDK 11
Тагир Валеев пришел к выводу, что конвейеры типа stream().skip(1).count() не являются O(1) для List::stream .
Это можно наблюдать, выполнив следующий тест:
|
1
2
3
4
|
@Benchmarkpublic long listStreamSkipCount() { return list.stream().skip(1).count();} |
|
1
2
3
4
5
6
|
CountBenchmark.listStreamCount 1 thrpt 5 105546649.075 ± 10529832.319 ops/sCountBenchmark.listStreamCount 1000 thrpt 5 81370237.291 ± 15566491.838 ops/sCountBenchmark.listStreamCount 1000000 thrpt 5 75929699.395 ± 14784433.428 ops/sCountBenchmark.listStreamSkipCount 1 thrpt 5 35809816.451 ± 12055461.025 ops/sCountBenchmark.listStreamSkipCount 1000 thrpt 5 3098848.946 ± 339437.339 ops/sCountBenchmark.listStreamSkipCount 1000000 thrpt 5 3646.513 ± 254.442 ops/s |
Таким образом, list.stream().skip(1).count() по-прежнему равен O (N).
Speedment
Некоторые реализации потоков на самом деле знают свои источники и могут использовать соответствующие ярлыки и объединять потоковые операции с самим источником потоков. Это может значительно повысить производительность, особенно для больших потоков с более сложными потоковыми конвейерами, такими как stream().skip(1).count()
Инструмент Speedment ORM позволяет просматривать базы данных как объекты Stream, и эти потоки могут оптимизировать многие потоковые операции, такие как
Операция Stream::count , Stream::skip , Stream::limit как показано в тесте ниже. В качестве ввода данных я использовал примерную базу данных Sakila с открытым исходным кодом. База данных Sakila полностью посвящена прокату фильмов, артистов и т. Д.
|
1
2
3
4
5
6
7
8
9
|
@Benchmarkpublic long rentalsSkipCount() { return rentals.stream().skip(1).count();}@Benchmarkpublic long filmsSkipCount() { return films.stream().skip(1).count();} |
При запуске будет получен следующий вывод:
|
1
2
|
SpeedmentCountBenchmark.filmsSkipCount N/A thrpt 5 68052838.621 ± 739171.008 ops/sSpeedmentCountBenchmark.rentalsSkipCount N/A thrpt 5 68224985.736 ± 2683811.510 ops/s |
Таблица «прокат» содержит более 10000 строк, тогда как таблица «фильм» содержит только 1000 строк. Тем не менее их операции stream().skip(1).count() завершаются практически в одно и то же время. Даже если таблица будет содержать триллион строк, она все равно будет считать элементы за то же время. Таким образом, реализация stream().skip(1).count() имеет сложность, которая составляет O(1) а не O(N) .
Примечание. Вышеприведенный тест выполнялся с ускорением «DataStore» в JVM-памяти. Если запускать без ускорения непосредственно для базы данных, время отклика будет зависеть от способности базовой базы данных выполнять вложенную “SELECT count(*) …” .
Резюме
Stream::count был значительно улучшен в Java 9.
Существуют реализации потоков, такие как Speedment, которые могут вычислять Stream::count за O(1) даже для более сложных потоковых конвейеров, таких как stream().skip(...).count() stream.filter(...).skip(...).count() stream().skip(...).count() или даже stream.filter(...).skip(...).count() .
Ресурсы
Инициализатор ускорения потока ORM: https://www.speedment.com/initializer/
Сакила: https://dev.mysql.com/doc/index-other.html или https://hub.docker.com/r/restsql/mysql-sakila
|
См. Оригинальную статью здесь: Поток Java: Часть 2. Счет всегда — счет? Мнения, высказанные участниками Java Code Geeks, являются их собственными. |