Почему array.push иногда быстрее, чем array [n] = value? - PullRequest
68 голосов
/ 05 марта 2009

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

Ответы [ 5 ]

81 голосов
/ 05 марта 2009

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

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

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

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

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

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

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

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

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
}

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

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

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

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

10 голосов
/ 02 марта 2010

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

в Safari:

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

в FireFox:

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

в IE7:

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

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

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

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

6 голосов
/ 05 марта 2009

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

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

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


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

4 голосов
/ 21 февраля 2013

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

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

0 голосов
/ 05 марта 2009

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

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