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