Почему array.push иногда быстрее, чем массив [n] = значение?

В качестве побочного результата тестирования некоторого кода я написал небольшую функцию для сравнения скорости использования метода array.push vs прямой адресации (array [n] = value). К моему удивлению, метод push часто проявлялся быстрее, особенно в Firefox, а иногда и в Chrome. Просто из любопытства: у кого есть объяснение? Вы можете найти тест @эту страницу (нажмите "Сравнение методов массивов" )

Ответы

Ответ 1

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

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

В вашем случае вы сначала устанавливаете последний элемент, что означает, что движок JS увидит массив, длина которого должна быть n, а только один элемент. Если n достаточно велико, это немедленно сделает массив редким массивом - в большинстве движков это означает, что все последующие вставки будут занимать случай медленного разреженного массива.

Вы должны добавить дополнительный тест, в котором вы заполняете массив от индекса 0 до индекса n-1 - он должен быть намного быстрее.

В ответ на @Christoph и из желания затягивать, здесь описание того, как массивы (как правило) реализованы в JS-специфике, варьируются от JS-движка до JS-движка, но общий принцип тот же.

Все JS Object (а не строки, числа, true, false, undefined или null) наследуются от базового типа объекта - точная реализация меняется, это может быть наследование С++ или вручную C (есть преимущества для этого в любом случае) - базовый тип объекта определяет методы доступа по умолчанию по умолчанию, например.

interface Object {
    put(propertyName, value)
    get(propertyName)
private:
    map properties; // a map (tree, hash table, whatever) from propertyName to value
}

Этот тип объекта обрабатывает всю стандартную логику доступа к объекту, цепочку прототипов и т.д. Тогда реализация массива станет

interface Array : Object {
    override put(propertyName, value)
    override get(propertyName)
private:
    map sparseStorage; // a map between integer indices and values
    value[] flatStorage; // basically a native array of values with a 1:1
                         // correspondance between JS index and storage index
    value length; // The `length` of the js array
}

Теперь, когда вы создаете Array в JS, движок создает нечто похожее на вышеуказанную структуру данных. Когда вы вставляете объект в экземпляр Array, метод Array put проверяет, является ли имя свойства целочисленным (или может быть преобразовано в целое число, например "121", "2341" и т.д.) Между 0 и 2 ^ 32 -1 (или, возможно, 2 ^ 31-1, я точно забыл). Если это не так, то метод put пересылается в базовую реализацию Object, а стандартная логика [[Put]] выполняется. В противном случае значение будет помещено в собственное хранилище Array, если данные будут достаточно компактными, тогда двигатель будет использовать хранилище с плоскими массивами, и в этом случае вставка (и извлечение) будет просто стандартной операции индексации массива, в противном случае движок преобразует массив к разреженному хранилищу и поместите/получите карту, чтобы получить от свойстваName значение location.

Я честно не уверен, что какой-либо движок JS теперь конвертируется из разреженного в плоское хранилище после этого преобразования.

Anyhoo, это довольно высокий уровень обзора того, что происходит, и не учитывает ряд более неприятных деталей, но общий шаблон реализации. Специфика того, как отправлено дополнительное хранилище, и как put/get отправляется, отличается от движка к движку - но это яснее, я могу действительно описать дизайн/реализацию.

Небольшая добавочная точка, в то время как спецификация ES ссылается на propertyName, поскольку строковые JS-двигатели имеют тенденцию специализироваться на поиске по целому числу, поэтому someObject[someInteger] не будет преобразовывать целое число в строку, если вы смотрите на объект, который имеет целочисленные свойства, например. Array, String и DOM (NodeList s и т.д.).

Ответ 2

Это результат, который я получаю с помощью вашего теста

в Safari:

  • Array.push(n) 1,000,000 значений: 0.124 сек
  • Массив [n.. 0] = значение (по убыванию) 1,000,000 значений: 3.697 сек
  • Массив [0.. n] = значение (по возрастанию) 1,000,000 значений: 0,073 с

в FireFox:

  • Array.push(n) 1,000,000 значений: 0,075 с
  • Массив [n.. 0] = значение (убывание) 1,000,000 значений: 1,193 с
  • Массив [0.. n] = значение (по возрастанию) 1,000,000 значений: 0,055 сек

в IE7:

  • Array.push(n) 1,000,000 значений: 2,828 с
  • Массив [n.. 0] = значение (убывание) 1,000,000 значений: 1.141 sec
  • Массив [0.. n] = значение (по возрастанию) 1,000,000 значений: 7.984 sec

В соответствии с вашим тестом метод push выглядит лучше на IE7 (огромная разница), а так как в других браузерах разница небольшая, кажется, что метод push действительно лучший способ для добавления элемента в массив.

Но я создал еще один простой тест script, чтобы проверить, какой метод быстро добавляет значения в массив, результаты действительно удивили меня, Использование Array.length кажется намного быстрее по сравнению с использованием Array.push, поэтому я действительно не знаю, что сказать или подумать больше, я не знаю.

BTW: на моем IE7 ваши остановки script и браузеры спрашивают меня, хочу ли я его продолжать (вы знаете типичное сообщение IE, в котором говорится: "Остановить запуск этого script?..." ) Я бы посоветовал немного уменьшить циклы.

Ответ 3

push() является частным случаем более общего [[Put]] и поэтому может быть дополнительно оптимизирован:

При вызове [[Put]] объекта массива сначала необходимо преобразовать аргумент в беззнаковое целое, потому что все имена свойств, включая индексы массива, являются строками. Затем его нужно сравнить с свойством длины массива, чтобы определить, нужно ли увеличивать длину. При нажатии, такое преобразование или сравнение не должно происходить: просто используйте текущую длину в качестве индекса массива и увеличьте его.

Конечно, есть другие вещи, которые повлияют на время выполнения, например, вызов push() должен быть медленнее, чем вызов [[Put]] через [], потому что цепочка прототипов должна быть проверена для первого.


Как отметил olliej: фактические реализации ECMAScript оптимизируют преобразование, т.е. для числовых имен свойств, преобразование из строки в uint не выполняется, а просто проверка типа. Основное предположение должно сохраняться, хотя его влияние будет меньше, чем я предполагал первоначально.

Ответ 4

Вот хороший тестовый стенд, который подтверждает, что прямое назначение значительно быстрее, чем push: http://jsperf.com/array-direct-assignment-vs-push.

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

Ответ 5

Push добавляет его до конца, а array [n] должен пройти через массив, чтобы найти нужное место. Вероятно, это зависит от браузера и способа обработки массивов.