Что быстрее: List или Vector и почему?

Скорость List и Vector в Scala зависит от типа операций, которые вы выполняете. Обе структуры — неизменяемые (immutable), но имеют разную внутреннюю архитектуру и разные целевые сценарии использования.

List: архитектура и особенности производительности

List — это односвязный список, построенный из узлов, где каждый элемент указывает на следующий (head :: tail). Это классическая структура в функциональном программировании.

Временные характеристики:

Операция Время выполнения
head O(1)
--- ---
tail O(1)
--- ---
prepend (добавить в начало) O(1)
--- ---
append (добавить в конец) O(n)
--- ---
Доступ по индексу O(n)
--- ---
Обход (итерация) O(n)
--- ---

Где List быстрее:

  • Когда часто добавляют элементы в начало.

  • Когда обходится последовательность от начала до конца.

  • При использовании рекурсии и pattern matching (head :: tail).

  • При создании небольших, статичных списков.

Vector: архитектура и особенности производительности

Vector — это дерево с фиксированной шириной 32, то есть дерево, где каждый узел содержит до 32 дочерних элементов. Глубина дерева — log₃₂(n), что даже при миллионах элементов означает глубину 5–6 уровней.

Временные характеристики:

Операция Время выполнения
apply(i) O(1) амортизированное
--- ---
update(i, v) O(1) амортизированное
--- ---
prepend и append O(log n)
--- ---
Обход O(n)
--- ---
head / last O(1)
--- ---

Где Vector быстрее:

  • При доступе по индексу.

  • При обновлении элементов по индексу.

  • При балансированных вставках в начало и конец.

  • При объединении и фильтрации больших коллекций.

  • При распараллеливании (в ParVector).

Сравнение по операциям

Операция List (время) Vector (время) Победитель
head O(1) O(1) одинаково
--- --- --- ---
tail O(1) O(log n) List
--- --- --- ---
apply(i) O(n) O(1) Vector
--- --- --- ---
update(i, v) невозможно O(1) Vector
--- --- --- ---
:+ (добавить в конец) O(n) O(log n) Vector
--- --- --- ---
:: (добавить в начало) O(1) O(log n) List
--- --- --- ---
map, filter O(n) O(n) одинаково
--- --- --- ---
reverse O(n) O(n) одинаково
--- --- --- ---
drop(n) O(n) O(log n) Vector
--- --- --- ---

Особенности работы GC и памяти

  • List создаёт много маленьких объектов (Cons-ячейки). Это может приводить к большему количеству сборок мусора.

  • Vector компактнее в памяти: хранит массивы с 32 ячейками, меньше объектов, более кэш-френдли.

  • Vector чаще предпочтительнее в нагруженных и реактивных системах.

Пример на практике

val list = List(1, 2, 3)
val vector = Vector(1, 2, 3)
list(2) // медленно: O(n)
vector(2) // быстро: O(1)
val l2 = 0 :: list // быстро
val v2 = 0 +: vector // медленно (O(log n))

Что использовать в зависимости от задачи

Сценарий Структура
Частое добавление в начало List
--- ---
Частый доступ по индексу Vector
--- ---
Обход и pattern matching List
--- ---
Работа с большими объёмами данных Vector
--- ---
Частое обновление элементов Vector
--- ---
Преимущ. функциональный стиль с рекурсией List
--- ---
Универсальное хранилище с хорошей производительностью Vector
--- ---

Синтетические тесты

Для обработки списков длиной до 10 элементов List быстрее в большинстве операций. Однако при размере 1000+ элементов Vector резко обходит List при случайном доступе и модификациях.

val list = (1 to 1000000).toList
val vector = (1 to 1000000).toVector
val t1 = System.nanoTime(); list(999999); println(System.nanoTime() - t1)
val t2 = System.nanoTime(); vector(999999); println(System.nanoTime() - t2)

В этом тесте vector(999999) выполнится на порядки быстрее, чем list(999999).

Выводы по производительности

  • List — лучше для неиндексируемых последовательных операций.

  • Vector — лучше для общего случая, где нужны и доступ по индексу, и модификации.

  • Для больших коллекций почти всегда Vector будет быстрее и эффективнее.

  • List выигрывает только тогда, когда вы исключительно добавляете в начало и обрабатываете head/tail.