Pull to refresh

Comments 28

Мне кажется, проще к своему текущему Java проекту добавить Scala. У меня старый код на Java остался, а все новое в проекте я пишу на Scala и легко вызываю старые вещи писаные на Java из Scala. Там синтаксис куда короче чем Stream от Java.
Вопрос так-то холиварный :-) Java — это Java, у неё свой путь, который отличается от Scala. Мне, как это ни странно, Java нравится больше. Пусть будет больше языков, выбор — это хорошо :-)
Кому как, синтаксис и философия Scala достаточно сильно отличается от Java, не все Java программисты готовы перейти на Scala. Scala все-таки не Java c новыми плюшками, каким вроде бы хочет стать Kotlin, Scala это Scala.
UFO just landed and posted this here
Ещё есть LazySeq, jOOλ, javaslang, cyclop-streams. Это всё другие библиотеки с другой философией. Кстати, если вы используете RxJava, перепишите примеры из статьи с её использованием. Интересно посмотреть.
Чем-то напоминает синтаксис работы со списками в Prolog)
Чем-то напоминает операцию свертки (fold) в Scala, я правда не уверен, насколько она там может быть промежуточной.
fold — это очень частный случай, похоже на пример с appendReduction, только исходный стрим не надо выдавать в результат:

public static <T> StreamEx<T> fold(StreamEx<T> input, T identity, BinaryOperator<T> op) {
    return input.headTail((head, tail) -> fold(tail, op.apply(identity, head), op), () -> Stream.of(identity));
}

Получается стрим из одного элемента, результата фолдинга. Это обычно терминальная операция, но можно и промежуточную вот так сделать, если надо.
Почему очень частный? С помощью fold также можно выразить большинство (или даже все) операций из статьи.
А, ну это всё-таки другое. В Stream API аналог — это комбинации коллекторов, конкретно для map — Collectors.mapping(mapper, Collectors.toList()). Это не ленивая операция, она сразу в массив собирает. Её к бесконечному стриму не применишь, короткое замыкание после неё не устроишь.
Хорошо, как же map через scan написать? Можно на js, если вам удобнее :-)
takeWhileInclusive, в смысле? Да, вариант. Я как раз думаю добавить такой метод непосредственно в StreamEx и парюсь, как его назвать :-)
Это все конечно хорошо для разминки мозгов, но как с производительностью? Эти кучи вызовов функций уйдут?
Вопрос закономерный и оверхед, безусловно, есть. Но в вопросе производительности всегда всё относительно. К примеру, возьмём массив из случайных 10000 строк (полученных из случайных чисел):

new Random(1).doubles(10000).mapToObj(String::valueOf).collect(Collectors.toList());

Скажем, вы с этими сроками собрались сделать «почти ничего» (например, промэпить сами на себя и сложить длины):

input.stream().map(x -> x).collect(Collectors.summingInt(String::length))

В этом случае реализация map через headTail работает в 17 раз медленнее (какой ужас!):

HeadTailTest.htSimple    avgt   30  1371,840 ±  32,914  us/op
HeadTailTest.jdkSimple   avgt   30    80,933 ±   5,080  us/op

Но делая почти ничего, вы не заработаете денег. Давайте что-нибудь более реалистичное. Например, удалять все единички, двойки и тройки регекспом и собирать результат в новый список:

input.stream().map(x -> x.replaceAll("[123]", "")).collect(Collectors.toList());

Теперь версия через headTail всего лишь на 30% медленнее обычной:

HeadTailTest.htComplex   avgt   30  7819,082 ± 208,534  us/op
HeadTailTest.jdkComplex  avgt   30  5999,423 ± 148,727  us/op

Бенчмарк здесь, можете сами поиграться. Кстати, люди, которые пишут регекспы, даже не задумываются, насколько они тормознее, чем сделать то же самое вручную, сконвертив строку в массив символов и пройдясь по ней циклом for. Берут и пользуются, потому что удобнее.

Разумеется, я не призываю использовать такой подход, если аналогичная операция уже существует. Но если надо что-то новенькое, он вполне оправдан. Это не какие-то там адские нереальные тормоза, вполне разумное замедление порядка 200 наносекунд на элемент стрима.
Тут проблема в том, что, имея такую универсальную операцию, люди перестанут включать мозг и задумываться, что здесь большой оверхед. И применять эти функции будут не только для задач, где он незаметен, но и в примерах по типу вашего первого, оправдываясь тем, что «ну ведь выглядит красиво».

А потом эти кусочки, каждый медленнее на порядок, чем он может быть, собираются в большую систему и тормоза уже начинают быть заметны. И да, 30% — это очень заметные тормоза. То, что делалось бы минуту, займет минуту и 18 секунд, то, что час — час и 18 минут. У меня за 18 минут проект с исходниками на 3 Гб собирается.

Сама идея похвальная, но в библиотеку я бы все же такое не добавлял, чтобы не было соблазна. Regexp-ы — как раз пример такого соблазна. Слишком легко не увидеть реальной стоимости операции (в случае в Java так еще и коварность есть, т.к. метод строки split принимает regexp, а не голую строчку). Да у вас в обзоре только 2 операции, которые совершенно новые, а остальные и так есть, поэтому зачем она? Вот бы были аннотации, которыми можно было бы аннотировать код, указывая его вычислительную стоимость и второй набор, которыми можно было бы ограничивать стоимость метода, чтобы всякие неоптимальные алгоритмы не пролезали куда не нужно…

Такие выкрутасы хороши тогда, когда язык может гарантировать, что все эти красивости соптимизируются, хотя бы все будет заинлайнено. В Rust-е такое наверняка бы прокатило.
По-моему, воспользоваться этой операцией, не включая мозг, не получится :-)

То есть вы бы не стали добавлять в язык регулярные выражения из опасения, что люди будут пихать их где ни попадя? Всегда есть баланс между удобством и производительностью, а преждевременные оптимизации — таки зло. Это при том, что я очень много внимания уделяю производительности и StreamEx — действительно быстрая библиотека.

У меня в обзоре много операций, которых нет в Stream API и в StreamEx: shuffle, reverse, mirror, every, scanLeft, dominators, appendReduction. Операции couples, withIndices, slides, batches в некотором виде есть, если источник — список со случайным доступом. Для произвольного списка нету. Операции scanLeft, dominators есть в виде терминальных, они не могут быть короткозамкнутыми.

Аннотации производительности — вопрос интересный, но это слишком серьёзная тема, чтобы обсуждать её в комментариях. Даже написать нормальную спецификацию для подобных аннотаций (при условии, чтобы они были действительно полезны) — большая академическая работа.
То есть вы бы не стали добавлять в язык регулярные выражения из опасения, что люди будут пихать их где ни попадя?

Нет, зачем выкидывать удобный инструмент? Но его использование должно быть явным, если это тяжелая операция. В случае со split-ом — он должен принимать не строку, а объект Pattern, если нужно разделение по regexp-у. Скрывать тяжелые операции за простым фасадом можно только в приватных функциях того же класса, если говорить применительно к Java — когда легко вычислить всех потребителей этого фасада.

А с этой вашей универсальной операцией легко создать ад по производительности и никто ничего не заметит, пока программа раком не станет. А в тот момент переписать более оптимально может быть уже затруднительно или не быстро.

Операции couples, withIndices, slides, batches в некотором виде есть, если источник — список со случайным доступом. Для произвольного списка нету.

Да, странное ограничение на произвольный доступ, вроде бы он этим операциям не нужен. Разве только когда обработка параллелиться должна, это имеет смысл.

У меня в обзоре много операций, которых нет в Stream API и в StreamEx

Как вы сами сказали, в вашей библиотеке только операции, обладающие некоторыми критериями качества. Если их нет даже в вашей библиотеке, это ли не признак того, что они не проходят даже по вашим меркам? А если проходят, то почему их нет, раз они так востребованы?

Вообщем, я к тому, что если вы хотите позиционировать свою библиотеку, как обладающую хорошей производительностью, нужно очень осторожно вносить в нее операции, которые выглядят, как производительные в одном ряду с другими операциями, но таковыми не являются. Не то начнут вместо специализированных и оптимизированных алгоритмов использовать ее направо и налево. Да вы и сами это подметили для регулярных выражений. Может, даже стоит пометить ее аннотацией @Deprecated, чтобы предупреждение от компилятора было.
Как вы сами сказали, в вашей библиотеке только операции, обладающие некоторыми критериями качества

Один из важнейших критериев в том, что операция должна параллелиться нормально. Все перечисленные отсутствующие операции плохо параллелятся (но это не имеет отношения к их производительности в последовательном режиме). Тем не менее они часто нужны на практике.

И насчёт ада вы преувеличиваете. Тем более сегодня производительность одна, завтра другая. Я могу существенно ускорить headTail. Ценой усложнения внутренней логики StreamEx и, возможно, весьма незначительного замедления некоторых других сценариев работы (но при этом некоторые другие сценарии могут и немного ускориться как побочный эффект). Если это будет востребовано (или просто мне самому приспичит), займусь. Вот с тем же сплитом вами нелюбимым (я его, кстати, тоже не люблю). В седьмой джаве добавили быструю обработку сплита по односимвольным литералам. Когда чем-то часто пользуются, разработчики начинают больше обращать внимания на производительность и ускоряют популярные сценарии. А если пользуются редко, то и никого не волнует.
Ну вот, пооптимизировал, уже не 17× и 30%, а 9× и 10%, теперь примерно 50-60 наносекунд и 152 байта аллокаций на элемент стрима оверхед (было где-то 120-200 наносекунд и 332 байта). Если по остальным сценариям не будет регрессии, включу этот код в новый релиз.

Кстати, не так много разработчиков чего бы то ни было вообще задаются вопросом, сколько у них байт аллокации и сколько реально времени их код съедает.
Sign up to leave a comment.

Articles