Как стать автором
Обновить

Комментарии 34

Спасибо за статью, она отличная. Не хватает только погрешностей в бенчмарке.
Добавил результаты в gist с погрешностями.

Ну хоть кто-то откомментировал! :-)
Спасибо, разрыв между streamExParallel и streamParallel впечатляет. А за счёт чего он достигается?
Точно не знаю. Могу предположить, что из-за трёхкратного чтения каждого элемента из списка — слишком много проверок границ. В принципе его можно переписать, чтобы третий раз не требовался, но будет менее красиво выглядеть. В моём случае из исходного списка чтение ведётся его родным сплитератором, каждый элемент один раз.
Другими словами, когда мне придётся работать не только со следующим элементом, но и с обоими соседями — StreamEx уже не поможет?
Да, текущая реализация работает конкретно с парами. Судя по моей практике, это самое частое, что требуется. Есть мысль добавить синтаксический сахар типа такого:

public static <T> StreamEx<List<T>> slide(List<T> source, int size) {
    return IntStreamEx.range(source.size()-size+1).mapToObj(idx -> source.subList(idx, idx+size));
}

То есть поток всех подсписков данного списка длины size. Тогда, например, задача из статьи будет так решаться:

slide(input, 2).filter(list -> list.get(0) < list.get(1)).map(list -> list.get(0)).toList();

Но это завязка на то, что источник — список с быстрым случайным доступом. Ну и всё же помедленнее будет работать для окна 2. Сделать же сплитератор, который хорошо параллелится и работает с окном произвольной длины не так тривиально. Да и придётся всё равно складывать элементы в промежуточный контейнер типа списка, это некрасиво. В общем, тут есть над чем подумать, многое может зависеть от пожеланий пользователей и их конкретных задач.
Ага, понял, спасибо за объяснение. Как мне кажется добавить реализацию хотя бы для трёх стоит — два крайних являются контекстом для центрального.
Придётся встать второй раз всё-таки. Мне кажется суперполезным было бы StreamEx.of(ResultSet). А то до JDBC функционадбные плюшки не добрались…

А, и в случае, если вы будете реализовывать slide — вероятно стоит использовать не листы, а Tuple2,3 итд. С аксессорами из серии get1, get2 итд.
С ResultSet вообще сложно. Во-первых, параллелить его нельзя. Во-вторых, там по факту один и тот же объект на каждой итерации, но в разном состоянии. Многие операции становятся бессмысленны. В-третьих, нередко его надо обязательно закрыть, то есть поток придётся оборачивать в try-with-resources. В-четвёртых, любая операция над ResultSet кидает SQLException, который checked. Вообще у JDBC интерфейс исключительно корявый. Эту проблему вроде неплохо пытается решить jOOQ, но проприетарщина.

У меня есть в приватном проекте метод с такой сигнатурой:

@FunctionalInterface
public interface ResultSetMapper<T>
{
    T map(ResultSet rs) throws SQLException;
}

public static <T> StreamEx<T> stream(Connection c, String query, ResultSetMapper<T> transformer) {...}

Предполагается, что мы сразу мэпим запись в ResultSet на что-нибудь другое и уже из этого делаем поток. Но это всё равно довольно коряво. Во всяком случае, в StreamEx я не буду пихать JDBC, это слишком специфичная штука.

И вот типы с туплами создавать не хочу, потому что это навязывание своих типов чужому приложению. В куче приложений и библиотек уже есть разнообразные туплы и создавать свои мне кажется коряво. По-моему, это не Java way. С pairMap я как раз обошёлся изящно — не стал новый тип создавать, а просто вызываю пользовательскую функцию, пользователь может использовать любой свой тип. А вот для троек уже пришлось бы объявлять свой функциональный интерфейс, потому что стандартного такого с тремя аргументами нет. Опять же надо чтобы была реальная задача, в которой этот функционал очень нужен, тогда будет понятнее, как его лучше реализовать. На пары самая типичная задача — получить попарные разности. Это легко решается с помощью StreamEx. А вот на тройки я пока не видел, где бы они пригодились…
Кстати, у создателей jOOQ есть такая штука, как jOOL. Там как раз есть рбота с JDBC. Но вот две библиотеки с разной реализацией функциональщины тянуть в проект не хочется…
О, спасибо, забыл совсем про jOOL. Она действительно на StreamEx похожа больше, чем protonpack, и имеет свою концепцию. Но тоже плюют на параллелизм даже там, где можно было бы не плевать. Плюс там есть вещи, которые в мою философию не вписываются. Например, метод Seq.reverse, который вычитывает поток в список и по этому списку создаёт новый поток (причём всегда последовательный вне зависимости от исходного). То есть это и не intermediate, и не terminal операция. Лучше уж коллектор написать типа toReverseList().

Вот над всякими skipWhile/limitWhile я давно думаю, но с параллелизмом они не дружат, поэтому, видимо, у меня их не будет. Но вообще пару идей оттуда можно стырить :-)
Мне кажется, что универсальность библиотеки важнее чистого параллелизма. Потому что выбор между универсальной и быстрой библиотеками не очевиден, в то время каку выбор между двумя универсальными библиотеками, одна из которых ещё и паралеллится — очевиден.

Кстати, ещё есть такой зверь, как javaslang. Дока там не очень, но интересные штуки есть. Хотя там всё-таки больше не про коллекции/стримы, а про монады с функторами.
Поглядел javaslang. Забавная штука. Сурово, что они свой класс назвали Stream, как бы призывая отказаться от стандартных стримов вообще.

Я каждую фичу в библиотеке взвешиваю со множества позиций. Насколько она вписывается в общую концепцию? Что потеряют люди, которые её не используют (как минимум вырастет объём библиотеки, и у них увеличится размер их приложения)? Насколько она полезна в реальной жизни? Есть ли альтернативные пути решения задачи, которая решается с помощью этой фичи? Мне кажется, сперва должна быть задача, а потом уже под неё затачивать инструмент, чем придумывать универсальный швейцарский нож на 124 инструмента, который трудно держать в руке и из которых 112 инструментов никому не нужны.

Вот свежий пример. Увидел, что в jOOL и в javaslang есть intersperse — вставить определённый элемент между каждой парой элементов потока. У меня в голове уже есть картинка, как это реализовать с помощью сплитератора. Будет хороший параллельный сплитератор, будет красивая intermediate-операция, которая укладывается в концепцию, супер. Но придётся писать эту операцию для четырёх типов потока (объектный и три примитивных), это увеличит итоговый jar килобайта на 3-4. А есть ли польза от этой операции? Единственное, что я могу придумать — это join строк с разделителем, но с этим легко справляется уже существующий коллектор. Если я найду в реальной жизни задачу, где intersperse полезен, или мне её кто-то покажет, я добавлю эту фичу.
Еще для полной картины есть такая реализация «идеи стримов с нуля»: GS collections. Посмотри API, идеи в реализации. Вот презентация, где они рассказывают, почему стримы сосут по сравнению с ними: www.slideshare.net/InfoQ/parallellazy-performance-java-8-vs-scala-vs-gs-collections
Спасибо, очень интересно.

С count() в Java 8 вообще смешно. Сделайте LongStream.range(0, 1000000000).count(), и он реально будет думать! В JDK 9 это всё переписали, к счастью — отдельную операцию сделали для подсчёта. Вероятно с девяткой результаты тестов уже будут другие.

Можно, кстати, попытаться переписать через forEach и LongAdder и посмотреть, сколько это займёт. Может, на восьмёрке и будет быстрее. Вообще на практике count не очень часто нужен.

Поддержка кастомных ForkJoinPool'ов у меня есть =)
Но придётся писать эту операцию для четырёх типов потока


А зачем? Я подобную задачу решал через генерацию. Причем, даже без непосредственно генератора. Писал одну объектную реализацию, а затем, по ней, генерил остальные.
Там не так много кода, чтобы заморачиваться с генерацией. Вон PairSpliterator гляньте, примерно такой же объём. Каждая специфичная реализация — строк 50, копипаста с заменой типов. Так что нет большой проблемы написать и поддерживать это вручную.

Другое дело, что это компилируется в 5 class-файлов, которые зипуются в jar независимо, поэтому в зазипованном виде это 5.8 Кб. Настолько из-за одной фичи pairMap увеличивается размер библиотеки и размер дистрибутива приложения, которое её использует, даже если pairMap не нужен. К этому надо ответственно подходить. Одно дело дополнительный метод, который съест байт 100, над ним можно долго не думать.
Я дико извиняюсь, но кого в наше время вообще волнует итоговый размер приложения? Хоть там мегабайт пуска будет — лишь бы работало хорошо и быстро…
Это пока Вы не попытаетесь запихнуть скалу в андроид.

Но в целом согласен, за исключением мобильных и embedded устройств, действительно пофиг на размер.
Со скалой всё не так просто, но дело тут вовсе не в размере. Дело там, ЕМНИП, в количестве классов, которое не умеет далвик. Раньше лечилось прогардом, думаю, что и сейчас тоже. А ежеди у вас один класс на 30 метров скомпилированного кода — то оно может и не очень хорошо, н андроид наверное справится.

/me задумался, как бы зафигачить такой класс и какого же размера будут исходники…
Почти. Не классы, а методы. И не далвик, а dex. Вот и вот. Так что формально, да, дело не в размере. Но по факту, что-то мне подсказывает, что зависимость «кол-во методов от размера байткода» линейная.
Ух ты, как меня память-то подвела. Ну 65к это тоже достаточно, как мне кажется.
А пять старушек — уже пять рублей. Я ставлю вопрос по-другому: зачем добавлять фичу, которая никому не нужна? Покажите хоть один реальный пример использования аналогичной функции в любом языке. Я честно рылся на GitHub. Везде или учебные примеры, или join строк.

Полезнее для практики, кстати, написать коллектор, который бы мог поток «Foo», «Bar», «Baz» собрать в строку «Foo, Bar, and Baz» со вставкой слова «and» перед последним элементом. Ещё полезно написать коллектор, который соберёт в «Foo, Bar, ...» при заданной максимальной длине строки. Думаю пока, можно ли всё это подружить с параллелизмом, какой конкретно должен быть интерфейс у методов и насколько это востребовано.
С другой стороны «под давлением общественности» я сделал foldRight/scanRight, хотя их можно начать выполнять только после того как закончатся выполняться все остальные шаги. По факту это нужно весьма редко, но раз есть foldLeft/scanLeft, то для симметрии стоило добавить. В jOOL, кстати, foldLeft ужасно реализован: они идут итератором по исходному потоку.

Ваш Сплитератор разделит поток [a, b, c, d] так [a, b], [b, c], [c, d]. А хочется, чтобы было так [a, b], [c, d]. Т.е. на один шаг больше.

Вам хочется решить другую задачу. Замечательно, решайте.

Blast from the past прямо :) Это ещё до того как ты в JetBrains работал пост написан, да?

Всё верно.

Да, я на него в статье сослался =)

Он мне совсем не нравится. Там концепции нет. Какое-то разрозненное месиво функционала. Ну и плюют на производительность и параллелизм.
"Особенно неравномерно делятся сплитераторы с бесконечным набором данных: после деления один из сплитераторов обрабатывает конечный объём, а второй остаётся бесконечным."

Возникла мысль о том, что есть лучшее решение: вводим счётчик объектов и, скажем, обменяемся Pipe`ами между сплитераторами — они заодно будут кешировать результаты. В итоге, допустим, чётные каждый раз берём себе, а нечётные отдаём "другу", ну и, естественно, "друг" — если успел быстрее — необорот, себе берёт нечётные, а чётные кладёт в Pipe. В принципе, можно и дальше так же сплитить — каждый из уже из достающихся ему, скажем, чётных элементов, так же вводит счётчик и у него тогда появляются свои "чётные" и "нечётные" — и т.д. Пытаюсь сейчас опробовать...
Проблема в упорядоченности: как потом результаты обработки данных из этого сплитератора сохранить в список с сохранением порядка? Скажем, тестовый пример:

Stream.iterate(0, x -> x + 1).parallel().map(x -> x*2).limit(100000).collect(Collectors.toList());

Должен выдать упорядоченный список.
Что же такое сплитератор

Сплитератор — это интерфейс, который содержит 8 методов, причём четыре из них уже имеют реализацию по умолчанию. Оставшиеся методы ...

Круть, интерфейс из 8 методов, боженька объясняет и заголовок рисует

Вне главы про объяснение, ЧТО такое сплитератор есть еще попытка объяснить:
Spliterator — это начинка потока, публичная часть его внутренней логики
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории