Scala Tuple2Zipped vs IterableLike zip - PullRequest
2 голосов
/ 09 июня 2019

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

val l1 = List(1,2,3)
val l2 = List(5,6,7)

val v1 = l1 zip l2
val v2 = (l1, l2).zipped

1 Ответ

5 голосов
/ 09 июня 2019

Если это не очевидно, значения и типы v1 и v2 различаются: v1 имеет тип List[(Int, Int)] со значением List((1, 5), (2, 6), (3, 7)); v2 имеет тип scala.runtime.Tuple2Zipped[Int, List[Int], Int, List[Int]] и имеет значение (List(1, 2, 3), List(5, 6, 7)).zipped.

Другими словами, значение v1 было вычислено строго (операция zip уже выполнена), тогда как v2 было вычислено лениво (или не строго ) - фактически операция zip была сохранена, но еще не выполнена.

Если все, что вы хотите сделать, это вычислить эти два значения (но не использовать их на самом деле), тогда я действительно ожидал бы, что v2 будет вычисляться быстрее, потому что на самом деле он не выполняет большую работу. ; -)

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

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

Оба подхода будут работать во всех случаях. (В общем случае я бы предпочел List.zip хотя бы потому, что Tuple2Zipped менее известен, и его использование намекает на специальное требование.)

Если производительность действительно вызывает озабоченность, то я рекомендую сравнить два подхода с вашим кодом, используя такой инструмент, как ScalaMeter и точно их различая. Я бы также порекомендовал тестирование использования памяти, а также времени обработки, поскольку у двух подходов разные требования к памяти.

ОБНОВЛЕНИЕ : Ссылка на дополнительный вопрос в комментариях ниже: "Есть ли разница между val m:Map[Int, Int] = (l1 zip l2)(breakOut) и (l1, l2).zipped.toMap?

Я повторю это следующим образом:

import scala.collection.breakOut

val l1 = List(1, 2, 3)
val l2 = List(5, 6, 7)

// m1's type has to be explicit, otherwise it is inferred to be
// scala.collection.immutable.IndexedSeq[(Int, Int)].
val m1: Map[Int, Int] = (l1 zip l2)(breakOut)
val m2 = (l1, l2).zipped.toMap

Нет такой вещи, как lazy Map, так как все элементы на карте должны быть доступны для внутренней структуры карты, что позволяет эффективно извлекать значения при выполнении поиск ключа.

Следовательно, различие между строго оцененным (l1 zip l2) и лениво оцененным (l1, l2).zipped исчезает в процессе преобразования в Map.

Так, что более эффективно? В этом конкретном примере я ожидаю, что оба подхода работают очень схожим образом.

При вычислении m1 операция zip повторяется через l1 и l2, проверяя пару элементов головы одновременно. Построитель breakOut (см. Также ссылку в комментарии ниже) и объявленный тип результата Map[Int, Int] заставляют операцию zip построить Map в качестве своего результата (без breakOut). zip приведет к List[(Int, Int)]).

Подводя итог этого подхода, полученная карта создается с помощью одного одновременного прохода через l1 и l2.

(Использование breakOut действительно имеет значение. Если мы сгенерировали карту как (l1 zip l2).toMap, то мы выполним одну итерацию через l1 и l2, чтобы создать List[(Int, Int)], затем выполните итерацию по этому списку создать результирующее Map, это явно менее эффективно.

В новом API коллекций Scala 13 breakOut был удален. Но есть новые альтернативы, которые работают лучше с точки зрения типа. См. этот документ для получения более подробной информации.)

Теперь давайте рассмотрим m2. В этом случае, как указывалось ранее, (l1, l2).zipped приводит к отложенному списку кортежей. Тем не менее, на данный момент, ни один итераций еще не было выполнено ни для одного из входных списков. Когда выполняется операция toMap, каждый кортеж в отложенном списке оценивается при первом обращении и добавляется создаваемая карта.

Подводя итог этому подходу, мы снова получаем, что полученная карта создается с помощью одного одновременного прохода через l1 и l2.

Итак, в данном конкретном сценарии использования между этими двумя подходами будет очень мало различий.Могут все еще быть незначительные детали реализации, которые влияют на результат, поэтому, если у вас есть огромное количество данных в l1 и l2, вы все равно можете сравнить их, чтобы найти лучшее решение.Однако я был бы склонен просто выбрать операцию zipbreakOut) и оставить все как есть.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...