Подробное изучение механизма реализации Immutable.js (1)

внешний интерфейс JavaScript React.js Immutable.js
Подробное изучение механизма реализации Immutable.js (1)

Эта статья - то, что я обновляюГлубокое погружение в immutable.jsПервый в серии.

Подробное изучение механизма реализации Immutable.js (1) Эта статья

Подробное изучение механизма реализации Immutable.js (2)

Immutable.js тратит много удобства на Facebook в течение 3 лет и предоставляет много удобства для фронтенд-разработки. Мы знаем, что Immutable.js принимает持久化数据结构, Чтобы гарантировать неизменность каждого объекта, любая операция добавления, изменения или удаления будет генерировать новый объект, а结构共享и другие способы значительно улучшить производительность.

В Интернете есть много статей, которые кратко знакомят с принципами работы с Immutable.js, но большинство из них — просто царапины.Есть несколько статей о реализации персистентных структур данных в Clojure или Go. Следующее объединяет различные источники, исходный код Immutable.js и мое собственное понимание для более глубокого изучения механизма реализации Immutable.js.

Эта серия статей может быть самой подробной и всеобъемлющей о принципах Immutable.js Добро пожаловать, чтобы поставить лайк и собрать σ`∀´)σ.

простой пример

Прежде чем углубиться в это, давайте рассмотрим простой пример:

let map1 = Immutable.Map({});

for (let i = 0; i < 800; i++) {
  map1 = map1.set(Math.random(), Math.random());
}

console.log(map1);

Этот код последовательно записывает в карту 800 пар случайно сгенерированных ключей и значений. Давайте сначала посмотрим на вывод консоли и получим общее представление о ее структуре данных (достаточно беглого взгляда):

Вы можете видеть, что это древовидная структура, а дочерние узлы размещены в свойстве `nodes` в виде массива. Максимальная длина `nodes`, кажется, равна 32. Люди, знакомые с растровыми изображениями, могли догадаться, что здесь делает свойство `bitmap`, оно включает в себя сжатие ширины дерева, о чем будет рассказано позже.

Один из узлов расширяется слой за слоем и выглядит так:

Этот `ValueNode` хранит набор значений, `entry[0]` — это ключ, а `entry[1]` — это значение.

Пока достаточно приблизительно посмотреть на форму, далее будем постепенно приоткрывать ее завесу от мелкого к глубокому. (Все свойства на графике будут объяснены во второй статье)

Фундаментальный

Давайте посмотрим на вики для持久化数据结构Определение:

In computing, a persistent data structure is a data structure that always preserves the previous version of itself when it is modified.

Простое объяснение заключается в том, что для持久化数据结构После каждой модификации мы получим новую версию, а старая версия может быть сохранена не повреждена.

Immutable.js реализован с деревьями持久化数据结构, сначала посмотрите на дерево ниже:

если бы мы былиgВставьте узел нижеh, как сохранить исходное дерево без изменений после вставки? Самый простой способ, конечно, перестроить дерево:

Но это заведомо неэффективно.Каждая операция должна генерировать новое дерево, что занимает много времени и места.Поэтому предлагается следующая схема оптимизации:

Мы генерируем новый корневой узел. Для измененной части регенерируем все узлы на соответствующем пути. Для части, которая не была изменена в этой операции, мы можем напрямую скопировать соответствующий старый узел. Это на самом деле结构共享. Таким образом, каждая операция также получит новую версию (корневой узел изменился, новыйa!== старыйa), исторические версии можно сохранить без изменений, сэкономив при этом место и время.

До сих пор мы обнаружили, что использование дерева для достижения持久化数据结构Все еще относительно простой, Immutable.js предоставляет различные структуры данных, например, вернемся к началу примера, как карта становится持久化数据结构Шерстяная ткань?

Vector Trie

На самом деле, для карты мы можем думать о ней как о плоском дереве, что совпадает с приведенной выше реализацией.持久化数据结构Точно так же после каждой операции генерируется новый объект, по очереди копируются все старые значения и перегенерируются свойства, которые необходимо изменить или добавить. Это на самом делеObject.assign, но это явно неэффективно, есть ли лучший способ?

в реализации持久化数据结构, ссылка на Immutable.jsVector TrieЭта структура данных (на самом деле более точное названиеpersistent bit-partitioned vector trieилиbitmapped vector trie, которая представляет собой структуру данных, используемую в Clojure, соответствующая реализация в Immutable.js очень похожа), давайте сначала разберемся с ее базовой структурой.

Если у нас есть карта, ключи все номера (конечно, вы также можете понять это как массив){0: ‘banana’, 1: ‘grape’, 2: ‘lemon’, 3: ‘orange’, 4: ‘apple’}, чтобы построить бинарное деревоVector Trie, мы можем сначала преобразовать все ключи в двоичную форму:{‘000’: ‘banana’, ‘001’: ‘grape’, ‘010’: ‘lemon’, ‘011’: ‘orange’, ‘100’: ‘apple’}, а затем создайте, как показано нижеVector Trie:

можно увидеть,Vector TrieКаждый узел - это массив0и1Два числа, представляющие собой двоичное число, все значения хранятся на листовых узлах, например, ищем001значение, просто следуйте0 0 1найти его, вы можете получитьgrape. тогда хочу добиться持久化数据结构Конечно это не сложно, например мы хотим добавить5: ‘watermelon’:

Видно, что для карты, ключами которой являются все числа, мы можем полностью передать картуVector Trieдобиться этого, достигнув持久化数据结构. Что делать, если ключ не является числом? Просто используйте механизм сопоставления, чтобы преобразовать его в число. Immutable.js реализуетhashФункция, которая преобразует значение в соответствующее число.

Чтобы упростить здесь, длина каждого массива узлов составляет всего 2, поэтому при большом количестве данных дерево станет очень глубоким, а запрос будет трудоемким, поэтому длину массива можно увеличить. Immutable.js выбрал 32. Почему не 31?40? На самом деле длина массива должна быть целой степенью числа 2, что предполагает реализациюVector TrieОптимизация времени, давайте сначала изучим это.

Некоторое из следующего содержания может быть относительно сложным для тех, кто не знаком с базовым преобразованием и манипулированием битами, но его можно понять, если хорошенько подумать.

Разделение цифр

数字分区Это означает, что мы сопоставляем ключ как число с деревом префиксов, как описано в предыдущем разделе.

Если у нас есть ключ9128, с основанием 7, то есть длина массива 7, он находится вVector TrieВот что он говорит:

Нам нужно 5 слоев массивов, сначала находим3эту ветку, а затем найти5, с последующим0. Чтобы получить эти числа последовательно, мы можем добавить9128преобразовать в 735420, но в этом нет необходимости, потому что процесс преобразования в шестнадцатеричный вид заключается в непрерывном делении и взятии остатка, чтобы получить число по каждой цифре, нам не нужно заранее преобразовывать его, и аналогичные операции можно выполнить над каждый слой по очереди.

Используя знания, связанные с базовым преобразованием, мы можем использовать этот метод key / radixlevel - 1% системы счисления, чтобы получить каждую цифру (Для краткости в этой статье все, кроме кода/Все символы обозначают деление и округление в меньшую сторону.),вradixдлина каждого слоя массива, то есть преобразованная в несколько знаков после запятой,levelЭто текущее количество слоев, то есть количество цифр. как здесьkeyда9128,radixда7, с началаlevelда5, по этой формуле мы можем получить номер первого слоя3.

Код реализован следующим образом:

const RADIX = 7;

function find(key) {
  let node = root; // root是根节点,在别的地方定义了

  // depth是当前树的深度。这种计算方式跟上面列出的式子是等价的,但可以避免多次指数计算。这个size就是上面的radix^level - 1
  for (let size = Math.pow(RADIX, (depth - 1)); size > 1; size /= RADIX) {
    node = node[Math.floor(key / size) % RADIX];
  }

  return node[key % RADIX];
}

Битовое разбиение

Очевидно, вышеизложенное数字分区Метод немного трудоемкий, в каждом слое приходится делать два деления и одно по модулю, что явно неэффективно.位分区Это его оптимизация.

位分区Установлено数字分区На основе всех целых степеней 2 (2, 4, 8, 16, 32...) в качестве базы数字分区дерево префиксов, может быть преобразовано в位分区. Обладая некоторыми знаниями о битовых операциях, мы можем избежать трудоемких вычислений.

数字分区Разделите ключ на числа и位分区Разделите ключ на группы битов. Возьмем в качестве примера 32-канальное дерево префиксов.数字分区Метод состоит в том, чтобы разделить ключ с основанием 32 (на самом деле это основание 32) и位分区Это разделить пять битов, так как 32 = 25, то мы можем рассматривать каждую цифру числа с основанием 32 как 5 двоичных цифр. На самом деле 32-значное число обрабатывается как двоичное число, так что многие исходные вычисления можно заменить более эффективными битовыми операциями. потому что база теперь равна 32, т.е.radixравно 32, поэтому предыдущее выражение теперь имеет ключ / 32level - 1% 32, а так как 32 = 25, то формулу можно записать в видеkey / 25 × (level - 1) % 25. Согласно знаниям, связанным с битовыми операциями, мы знаем, чтоa / 2n === a >>> n ,a % 2n === a & (2n - 1) . Таким образом, мы можем получить значение этой формулы с помощью побитовых операций.

Если вы не знакомы с битовой операцией, не смотрите на приведенную выше формулу, просто приведите пример, чтобы понять: например, числа666666Двоичная форма 101000101100001 01010 — 20-битное двоичное число. Если мы хотим получить пять цифр второго слоя01011, мы можем сначала сдвинуть его вправо>>>(Левая сторона заполнена 0) 10 цифр, получаем 00000 00000 1010001011,Снова&00000 00000 00000 11111, вы получаете01011.
Таким образом, мы можем получить следующий код:

const BITS = 5;
const WIDTH = 1 << BITS, // 25 = 32
const MASK = WIDTH - 1; // 31,即11111

function find(key) {
  let node = root; 

  for (let bits = (depth - 1) * BITS; bits > 0; bits -= BITS) {
    node = node[(key >>> bits) & MASK];
  }

  return node[key & MASK];
}

Это увеличит скорость каждого поиска. Вы можете посмотреть на картинку для понимания.Для упрощения отображения предположим, что мы используем 4-стороннее префиксное дерево, 4 = 22, поэтому разделите на две двоичные цифры. за626, процесс поиска выглядит следующим образом:

626Бинарная форма10 01 11 00 10, поэтому с помощью описанного выше метода битовой операции мы можем эффективно получить10,01

исходный код

Сказав это, давайте взглянем на исходный код Immutable.js. Нам достаточно в основном смотреть на ту часть поиска, котораяVector TrieОсновной.

get(shift, keyHash, key, notSetValue) {
  if (keyHash === undefined) {
    keyHash = hash(key);
  }
  const idx = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
  const node = this.nodes[idx];
  return node
    ? node.get(shift + SHIFT, keyHash, key, notSetValue)
    : notSetValue;
}

Видно, что Immutable.js также использует метод побитового разделения и выбирает соответствующую ветвь, получая индекс текущего массива посредством битовой операции. (Я не могу не поразиться, сколько идей содержится всего в 10 строках кода)

Однако его реализация немного отличается от приведенной выше.Для приведенного выше ключа мы храним его в «положительном порядке», например, как на картинке выше.626Например, мы следуем шагам от корневого узла вниз10 01 11 00 10Для хранения и Immutable.js является «обратным порядком», согласно10 00 11 01 10место хранения. Итак, по исходному коду вы обнаружите, что при поиске Immutable.js сначала получаются биты SHIFT в конце ключа, а затем биты SHIFT перед ними, а затем идут вперед по очереди, и предыдущий код первым получает ключ в начале битов SHIFT и т.д.Одна из причин такого подхода заключается в том, что размер ключа (двоичная длина) не является фиксированным.

временная сложность

из-за принятия结构共享, после операций добавления, изменения и удаления мы избегаем копирования всех значений в карте, поэтому, особенно когда объем данных велик, эти операции относительноObject.assignЗначительно улучшен.

Тем не менее, скорость запроса, кажется, замедляется? Мы знаем, что скорость поиска по ключу в карте равнаO(1), здесь, поскольку он становится деревом, временная сложность запроса становитсяO(log N), так как это 32-арное дерево, оно равно O(log32Н).

Подожди 32-аристое дерево? Это дерево не очень широкое. Максимальное количество ключей, которые могут иметь объект в JavaScript, как правило, не более 232Кусок(ECMA-262 Пятое изданиеВ JS определено, что, поскольку длина самого массива представляет собой 32-значное число, длина массива не должна превышать 232- 1. Реализация объектов в JS относительно сложна, но большинство функций основано на массивах, поэтому в большинстве сценариев количество ключей в объекте не будет превышать 232- 1. Соответствующее обсуждение см.здесь. И пусть у нас есть 232Каждое значение представляет собой 32-битное число. Если считать только эти значения, общий размер составляет 17 г. Переднему интерфейсу вообще не нужно оперировать данными такого масштаба), так что временную сложность поиска можно считать как " O (лог32 232)», что равно почти «O(7)», поэтому мы можем думать, что на практике временная сложность 5-битного (32-канального) запроса векторного дерева постоянна, а 32-арное дерево использует пространство для времени.

Пространство... Это 32-арочное дерево занимает слишком много места, верно? Даже имея всего три этажа, у нас будет больше, чем32 × 32 × 32 = 32768узел. Конечно, Immutable.js не будет тупо занимать такое большое место в своей конкретной реализации, он "сжимает" высоту и ширину дерева, кроме того, он также делает некоторые другие оптимизации для эффективности работы. Мы обсудим соответствующий контент в следующей статье.

Если в статье есть ошибки, поправьте меня.

Во второй части будут представлены дополнительные оптимизированные неизменяемые структуры данных —HAMT(«сжатое» занимаемое пространство) и изменяемые структуры, реализующие «временное» в неизменяемых структурах данных —Transient, и клише охэш-коллизиярешение.

Подробное изучение механизма реализации Immutable.js (1) Эта статья

Подробное изучение механизма реализации Immutable.js (2)

Время от времени я пишу статьи, которые могут быть полезными, прошу обратить внимание!

Ссылаться на:

Также нравится ion.com/musings/UN…

IO-meter.com/2016/09/03/…

CDN.O Рейли static.com/en/assets/1…

Michael.Stein's or Phil.Then/публикация…

GitHub.com/Facebook/IM…