Автор оригинала: Kotlin Academy.
Первоначально опубликовано по адресу: blog.kotlin-academy.com по Мартин Москвич
Люди часто упускают из виду разницу между Повторяемость
и Последовательность
. Это объяснимо. Особенно когда вы сравниваете Повторяемость
и Последовательность
определение:
interface Iterable{ operator fun iterator(): Iterator } interface Sequence { operator fun iterator(): Iterator }
Можно сказать, что единственное формальное различие между ними-это название. Но Повторяемость
и Последовательность
связана с совершенно другим использованием, поэтому почти все их функции обработки работают по-другому.
Последовательности являются ленивыми, поэтому промежуточные функции для обработки Последовательности
не выполняют никаких вычислений. Вместо этого они возвращают новые Последовательность
, которая украшает предыдущую новую операцию. Все эти вычисления оцениваются во время работы терминала, как для перечисления
или посчитайте
. С другой стороны, функции для Повторяемая
обработка возвращает новую коллекцию.
fun main(args: Array) { val seq = sequenceOf(1,2,3) print(seq.filter { it % 2 == 1 }) // Prints: kotlin.sequences.FilteringSequence@XXXXXXXX print(seq.filter { it % 2 == 1 }.toList()) // Prints: [1, 3] val list = listOf(1,2,3) print(list.filter { it % 2 == 1 }) // Prints: [1, 3]}
Фильтр последовательности
является промежуточной операцией, поэтому он не выполняет никаких вычислений, а вместо этого украшает последовательность новым шагом обработки. Вычисления выполняются в терминальной операции, например для перечисления
.
Из-за этого также отличается порядок обработки операций. При последовательной обработке мы обычно выполняем полную обработку для одного элемента, затем для другого и т. Д. В Итеративная
обработка, мы обрабатываем всю коллекцию на каждом шаге.
sequenceOf(1,2,3) .filter { println("Filter $it, "); it % 2 == 1 } .map { println("Map $it, "); it * 2 } .toList() // Prints: Filter 1, Map 1, Filter 2, Filter 3, Map 3, listOf(1,2,3) .filter { println("Filter $it, "); it % 2 == 1 } .map { println("Map $it, "); it * 2 } // Prints: Filter 1, Filter 2, Filter 3, Map 1, Map 3,
Благодаря этой лени и порядку обработки каждого элемента мы можем сделать инфинитив Последовательность
.
generateSequence(1) { it + 1 } .map { it * 2 } .take(10) .forEach(::print) // Prints: 2468101214161820
Это не должно быть чем-то новым для разработчика Kotlin с некоторым опытом, но есть еще один важный факт о последовательностях, который не упоминается в большинстве статей или книг: последовательности более эффективны для обработки коллекции с более чем одним шагом обработки.
Что я подразумеваю под более чем одним этапом обработки? Я имею в виду больше, чем одну функцию для обработки коллекции. Итак, если вы сравните эти две функции:
fun singleStepListProcessing(): List{ return productsList.filter { it.bought } } fun singleStepSequenceProcessing(): List { return productsList.asSequence() .filter { it.bought } .toList() }
Вы заметите, что почти нет разницы в производительности или простая обработка списка выполняется быстрее ( потому что она встроенная
). Хотя затем вы сравниваете функцию с несколькими шагами обработки, например двухэтапная обработка списка
, которая использует фильтр
и тогда карта
, разница будет видна.
fun twoStepListProcessing(): List{ return productsList .filter { it.bought } .map { it.price } } fun twoStepSequenceProcessing(): List { return productsList.asSequence() .filter { it.bought } .map { it.price } .toList() } fun threeStepListProcessing(): Double { return productsList .filter { it.bought } .map { it.price } .average() } fun threeStepSequenceProcessing(): Double { return productsList.asSequence() .filter { it.bought } .map { it.price } .average()
Насколько это важно? Давайте посмотрим среднее время работы по результатам контрольных измерений:
twoStepListProcessing 81 095 ns/op twoStepSequenceProcessing 55 685 ns/op twoStepListProcessingAndAcumulate 83 307 ns/op twoStepSequenceProcessingAndAcumulate 6 928 ns/op
Двухэтапная обработка уже заметно ускоряется, когда мы используем Последовательности
. В этом случае улучшение составляет около 30%.
Эта разница возрастает еще больше, когда мы накапливаемся в количестве, а не в каком-то списке. В таком случае вообще не нужно создавать промежуточную коллекцию.
Как насчет какой-нибудь типичной обработки в реальной жизни? Допустим, нам нужно рассчитать среднюю цену продуктов, купленных взрослыми:
fun productsListProcessing(): Double { return clientsList .filter { it.adult } .flatMap { it.products } .filter { it.bought } .map { it.price } .average() } fun productsSequenceProcessing(): Double { return clientsList.asSequence() .filter { it.adult } .flatMap { it.products.asSequence() } .filter { it.bought } .map { it.price } .average() }
Вот результаты:
SequencesBenchmark.productsListProcessing 712 434 ns/op SequencesBenchmark.productsSequenceProcessing 572 012 ns/op
У нас улучшение примерно на 20%. Это меньше, чем при прямой обработке (без flatMap
), но все равно является важным отличием.
Это показывает общее правило, которое можно найти снова и снова при измерении производительности:
Обработка последовательности, как правило, выполняется быстрее, чем обработка прямого сбора, когда у нас более одного шага обработки.
Когда последовательности не становятся быстрее?
Есть некоторые редкие операции, при которых мы не получаем прибыли от Последовательности
использования, потому что нам приходится работать со всей коллекцией эфирным способом. сортировка
является примером из Kotlin stdlib (я считаю, что это единственная функция в Kotlin stdlib, у которой есть эта проблема).
сортировка
использует оптимальную реализацию: она накапливает Последовательность
в Список
а затем использует сортировку
из Java stdlib. Недостатком является то, что этот процесс накопления занимает дополнительное время, если мы сравним его с той же обработкой в Коллекции
( хотя, если Итерируемый
не является Коллекцией
или массивом, тогда разница не существенна, потому что ее также необходимо накапливать).
Это спорно, если Последовательность
должна иметь такие методы, как сортировка
, потому что она только визуально ленива (оценивается, когда нам нужно получить первый элемент) и не работает с инфинитивными последовательностями. Он был добавлен, потому что это популярная функция, и ее намного проще использовать таким образом. Хотя разработчик Kotlin должен помнить, что это нельзя использовать в последовательностях инфинитивов.
generateSequence(0) { it + 1 }.sorted().take(10).toList() // Infinitive calculation time generateSequence(0) { it + 1 }.take(10).sorted().toList() [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
сортировка
является редким примером этапа обработки, который выполняется быстрее на Коллекция
не
Последовательность . Тем не менее, когда мы выполняем несколько шагов обработки и одну
отсортированную функцию (или другую функцию, которая должна работать со всей коллекцией), мы можем ожидать повышения производительности от перехода к
Последовательности
// Took around 150 482 ns fun productsSortAndProcessingList(): Double { return productsList .sortedBy { it.price } .filter { it.bought } .map { it.price } .average() } // Took around 96 811 ns fun productsSortAndProcessingSequence(): Double { return productsList.asSequence() .sortedBy { it.price } .filter { it.bought } .map { it.price } .average() }
А как насчет потока Java?
Java 8 представила потоки, позволяющие обрабатывать коллекции. Они действуют и выглядят аналогично последовательностям Котлина.
productsList.asSequence() .filter { it.bought } .map { it.price } .average() productsList.stream() .filter { it.bought } .mapToDouble { it.price } .average() .orElse(0.0)
Они также ленивы и собираются на последнем (терминальном) этапе обработки. Потоки Java также более эффективны для обработки коллекций, чем прямые функции обработки коллекций Kotlin. Два больших различия между парами Java и последовательностями Котлина заключаются в следующем:
- Последовательности Kotlin имеют гораздо больше функций обработки (потому что они определены как функции расширения), и их использование, как правило, проще (это результат того, что последовательности Kotlin были разработаны, когда уже использовались потоки Java — например, мы можем собирать с помощью
ToList()
вместоcollect(Коллекторы.тоЛист())
) - Потоки Java можно запускать в параллельном режиме с помощью функции
parallel
. Это может значительно повысить производительность в условиях, когда у нас есть машина с несколькими ядрами (что сегодня является стандартом). - Последовательности Kotlin могут использоваться в общих модулях, модулях Kotlin/JS и Kotlin/Native.
За исключением того, что, когда мы не используем параллельный режим, трудно дать простой ответ, является ли последовательность Java steam или Kotlin более эффективной.
Мое предложение состоит в том, чтобы использовать потоки Java только для вычислительно тяжелой обработки, где вы можете извлечь выгоду из параллельного режима. В противном случае используйте функции Kotlin stdlib, чтобы получить однородный и чистый код.
Эффективный Котлин
Это первая статья об эффективном Котлине. Когда мы увидим интерес, мы опубликуем следующие части. В Академии Котлина мы также работаем над книгой на эту тему:
Эффективный Котлин _ В этой книге представлен глубокий анализ передовых практик, как официальных (лучшие практики Котлина и Google для Котлина), так и and…_leanpub.com
Она будет охватывать гораздо более широкий круг тем и гораздо глубже вникать в каждую из них. В него также войдут лучшие практики, опубликованные Kotlin и командой Google, опыт членов команды Kotlin, с которыми мы сотрудничаем, и темы, затронутые в серии “Эффективная Java в Kotlin”. Чтобы поддержать его и ускорить публикацию, воспользуйтесь этой ссылкой и подпишитесь .
Если вам нужна помощь с Котлином, помните, что Я даю консультации .
Чтобы быть в курсе отличных новостей Kotlin Academy , подписывайтесь на рассылку новостей , следите за Твиттером и следите за новостями.
Чтобы ссылаться на меня в Твиттере, используйте @MarcinMoskala . Используйте ссылку ниже, чтобы подписаться на рассылку новостей:
Оригинал: “https://www.codementor.io/@kotlin_academy/effective-kotlin-use-sequence-for-bigger-collections-with-more-than-one-processing-step-jqgbmnllp”