Ну, это действительно зависит от того, что фактический тип карты вы используете. Возможно HashMap
. Теперь, изменяемые структуры, такие как производительность, обеспечивают предварительную выделение памяти, которую он ожидает использовать. Вы присоединяетесь к одному миллиону карт, поэтому окончательная карта должна быть несколько большой. Давайте посмотрим, как эти ключевые/значения добавляются:
protected def addEntry(e: Entry) {
val h = index(elemHashCode(e.key))
e.next = table(h).asInstanceOf[Entry]
table(h) = e
tableSize = tableSize + 1
if (tableSize > threshold)
resize(2 * table.length)
}
Смотрите 2 *
в resize
линии? Изменчивый HashMap
растет с удвоением каждый раз, когда у него заканчивается пространство, а неизменяемый - довольно консервативный в использовании памяти (хотя существующие ключи обычно занимают в два раза больше места при обновлении).
Теперь, как и для других проблем с производительностью, вы создаете список ключей и значений в первых двух версиях. Это означает, что до того, как вы присоединитесь к любым картам, вы уже имеете каждый Tuple2
(пары ключ/значение) в памяти дважды! Плюс накладные расходы List
, что мало, но мы говорим о более чем одном миллионе элементов раз накладные расходы.
Возможно, вы захотите использовать проекцию, которая позволяет избежать этого. К сожалению, прогноз на основе Stream
, что не так надёжёво для наших целей на Scala 2.7.x. Тем не менее, попробуйте вместо этого:
for (m <- listOfMaps.projection; kv <- m) yield kv
Stream
не вычисляет значение до тех пор, пока это необходимо. Сборщик мусора должен также собирать неиспользуемые элементы, если вы не держите ссылку на голову Stream
, что, по-видимому, имеет место в вашем алгоритме.
EDIT
Дополняя, а при/выход понимание принимает один или несколько коллекций и возвращает новую коллекцию. Так часто, как это имеет смысл, возвращаемая коллекция имеет тот же тип, что и исходная коллекция. Так, например, в следующем коде for-comprehension создает новый список, который затем сохраняется внутри l2
. Это не val l2 =
, который создает новый список, но для понимания.
val l = List(1,2,3)
val l2 = for (e <- l) yield e*2
Теперь давайте посмотрим на код используется в первых двух алгоритмов (минус mutable
ключевое слово):
(Map[A,B]() /: (for (m <- listOfMaps; kv <-m) yield kv))
foldLeft
оператор, здесь написано с его /:
синонимами, будет вызван на объект, возвращенный понятием. Помните, что :
в конце оператора инвертирует порядок объекта и параметры.
Теперь давайте рассмотрим, что это за объект, на который вызывается foldLeft
. Первый генератор в этом понимании - m <- listOfMaps
. Мы знаем, что listOfMaps
представляет собой набор типов List [X], где X здесь не имеет особого значения. Результатом по-осмысления на List
всегда является другой List
. Другие генераторы не имеют отношения к делу.
Итак, вы берете этот List
, получить все ключ/значение внутри каждого Map
, который является составной частью этого List
, и сделать новый List
со всем этим. Вот почему вы дублируете все, что у вас есть.
(на самом деле, это даже хуже, чем это, потому что каждый генератор создает новую коллекцию, коллекции, созданные с помощью второго генератора только размер каждого элемента listOfMaps
хотя, и сразу же после использования выбрасывается)
Следующий вопрос - на самом деле, первый, но было проще инвертировать ответ - так помогает использование projection
.
Когда вы звоните projection
на List
, он возвращает новый объект, тип Stream
(на Scala 2.7.x). Сначала вы можете подумать, что это только усугубит ситуацию, потому что теперь у вас будет три копии List
, а не один. Но Stream
не предварительно вычисляется. Это ленивый расчет.
Что это означает, что полученный объект, Stream
, не является копией List
, но, скорее, функция, которая может быть использована для вычисления Stream
при необходимости. После вычисления результат будет сохранен, чтобы его не нужно было вычислять снова.
Кроме того, map
, flatMap
и filter
из Stream
все вернуть новый Stream
, а значит, вы можете приковать их всех вместе, не делая одну копию List
, который их создал. Поскольку для -понимания с yield
используют эти самые функции, использование Stream
внутри предотвращает ненужные копии данных.
Теперь предположим, что вы написали что-то вроде этого:
val kvs = for (m <- listOfMaps.projection; kv <-m) yield kv
(Map[A,B]() /: kvs) { ... }
В этом случае вы не увеличиваясь ничего. После присвоения Stream
до kvs
данные еще не скопированы. Однако, если вторая строка выполнена, kvs вычислит каждый из своих элементов и, следовательно, будет содержать полную копию данных.
Теперь рассмотрим оригинальную форму ::
(Map[A,B]() /: (for (m <- listOfMaps.projection; kv <-m) yield kv))
В этом случае Stream
используется в то же время он вычислен. Давайте кратко рассмотрим, как foldLeft
для Stream
определяется:
override final def foldLeft[B](z: B)(f: (B, A) => B): B = {
if (isEmpty) z
else tail.foldLeft(f(z, head))(f)
}
Если Stream
пуст, просто верните аккумулятор. В противном случае вычислите новый аккумулятор (f(z, head)
), а затем передайте его и функцию на tail
Stream
.
Как только f(z, head)
выполнил, однако, не будет никакой ссылки на head
. Или, другими словами, ничто в программе не будет указывать на head
Stream
, а это значит, что сборщик мусора может его собирать, тем самым освобождая память.
Конечным результатом является то, что каждый элемент, созданный по-понятию, будет существовать только ненадолго, в то время как вы используете его для вычисления аккумулятора. И вы сохраняете копию всех ваших данных.
Наконец, возникает вопрос, почему третий алгоритм не извлекает из этого выгоду. Ну, третий алгоритм не использует yield
, поэтому никакой копии каких-либо данных не производится. В этом случае использование projection
добавляет только слой косвенности.
Интересно. Я последовал вашим советам и переключил списки на потоки, и улучшения производительности не было. Фактически, императивная реализация удвоилась за требуемое время. Однако измененные реализации перестали заканчиваться из памяти. – Jeff
Также, возможно, это вопрос noobish, но как первые две версии создают списки ключей и значений? Это потому, что понимание for() дает новый список? Я не понимаю, как вызывать вызов .projection() внутри выражения for, также избегает этого. Это потому, что понимание возвращает тот же тип последовательности, что и передан? – Jeff
Я дополню свой ответ, поскольку это слишком много, чтобы оставить комментарий. –