Действительно ли чтение свойства `length` массива является настолько дорогостоящей операцией в JavaScript?

#javascript #arrays #micro-optimization

#javascript #массивы #микрооптимизация

Вопрос:

Я всегда предполагал, что кэширование длины массива в JavaScript является хорошей идеей (особенно в состоянии for цикла) из-за дороговизны вычисления длины массива.

Пример

 for (var i = 0; i < arr.length; i  ) { }

// vs

for (var i = 0, arrLength = arr.length; i < arrLength; i  ) { }
  

Однако я подумал, что, возможно, length свойство обновляется только при создании и изменении массива. Следовательно, его чтение не должно быть слишком дорогостоящей операцией в отличие от чтения его, хранящегося в переменной (в отличие от других методов на других языках, которым может потребоваться поиск в памяти, чтобы найти конец чего-либо, например strlen() в C).

У меня есть два вопроса. Мне также интересно, как это работает, поэтому, пожалуйста, не обвиняйте меня в преждевременной оптимизации.

Предположим, что движки JavaScript в браузерах.

  1. Есть ли какое-либо преимущество в кэшировании length свойства массива в JavaScript? Намного ли сложнее читать локальную переменную поверх свойства объекта?
  2. length Свойство просто изменяется при создании и в методах типа shift() и pop() , которые не возвращают новый массив и в противном случае просто сохраняются как целое число?

Ответ №1:

Ну, я бы сказал, что это дорого, но затем я написал небольшой тест @ jsperf.com и, к моему удивлению, использование i<array.length на самом деле было быстрее в Chrome, а в FF (4) это не имело значения.

Я подозреваю, что длина хранится как целое число (Uint32). Из ECMA-specs (262 изд. 5, стр. 121):

Каждый объект массива имеет свойство length, значением которого всегда является неотрицательное целое число, меньшее 232. Значение свойства length численно больше, чем имя каждого свойства, имя которого является индексом массива; всякий раз, когда создается или изменяется свойство объекта Array, другие свойства корректируются по мере необходимости для поддержания этого инварианта. В частности, всякий раз, когда добавляется свойство, имя которого является индексом массива, свойство length при необходимости изменяется на единицу больше числового значения этого индекса массива; и всякий раз, когда изменяется свойство length, каждое свойство, имя которого является индексом массива, значение которого не меньше новой длины, автоматически удаляется. Это ограничение применяется только к собственным свойствам объекта Array и не зависит от свойств длины или индекса массива, которые могут быть унаследованы от его прототипов

Уф! Я не знаю, привыкну ли я когда-нибудь к такому языку…

Наконец, у нас всегда есть наш старый добрый браузер, который отстает. В IE (9, 8, 7) кэширование длины происходит действительно быстрее. Я говорю, что это одна из многих других причин не использовать IE.

Комментарии:

1. В моем Chrome 10.0.648.127 на Linux i686 это также не имело значения.

2. Массивы в языках сценариев (Ruby, Perl, …) обычно знают две длины в виде простых целых чисел: сколько слотов было выделено и сколько слотов используется (т. Е. Длина). Любой, кто сделал бы это по-другому в движке JavaScript, вероятно, не должен быть допущен к компилятору 🙂 Массивы обычно знают, сколько у них времени, не вычисляя его. За исключением C, конечно.

3. @Joachim Sauer. Предположим, что это правда. В любом случае, почему написание простого, понятного кода должно быть заявлено в качестве правила Java?

4. Это не просто правило Java, но в Java оно иногда цитируется по соображениям производительности . Есть часто цитируемая статья под названием «Write Dumb Code» .

5. Я бы сказал, что, поскольку кэширование длины действительно имеет значение в IE, а IE (по крайней мере, до IE 9) является чрезвычайно медленным браузером, такая оптимизация может быть полезной.

Ответ №2:

TL; DR:

Из того, что я могу собрать, кажется, что длина массива кэшируется внутри (по крайней мере, в версии 8)..

(Подробности? Читайте дальше :))

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

Копаясь в исходном коде версии 8, мы получили класс JsArray.

 // The JSArray describes JavaScript Arrays
//  Such an array can be in one of two modes:
//    - fast, backing storage is a FixedArray and length <= elements.length();
//       Please note: push and pop can be used to grow and shrink the array.
//    - slow, backing storage is a HashTable with numbers as keys.
  

Я делаю предположение, что тип элементов массива определяет, будет ли это быстро или медленно. Я добрался до того, что в set_has_fast_elements ( set_bit_field2(bit_field2() | (1 << kHasFastElements)) ) был установлен флаг bit, в котором я решил нарисовать линию поиска, поскольку я искал в Google code и не имел локального источника.

Теперь, кажется, что в любое время с массивом выполняется любая операция (которая является дочерним классом JSObject , выполняется вызов NormalizeElements() , который выполняет следующее:

 // Compute the effective length.
  int length = IsJSArray() ?
      Smi::cast(JSArray::cast(this)->length())->value() :
      array->length();
  

Итак, отвечая на ваши вопросы:

  1. Похоже, что в Chrome (или других браузерах, использующих V8) нет никаких преимуществ в кэшировании length свойства массива (если только вы не делаете что-то странное, что заставило бы это сделать slow (я не уверен, каковы эти условия) — сказав это, я, скорее всего, продолжу кэшировать length , пока не получу возможность просмотреть все реализации браузера OS 😉
  2. length Свойство, похоже, изменяется после любой операции над объектом.

Редактировать:

Кстати, кажется, что «пустой» массив на самом деле выделяется для 4 элементов:

 // Number of element slots to pre-allocate for an empty array.
static const int kPreallocatedArrayElements = 4;
  

Я не уверен, на сколько элементов увеличивается массив после превышения границ — я не копал так глубоко 🙂

Комментарии:

1. Типичные реализации увеличивают массив на постоянную величину, кратную размеру массива — обычно путем удвоения, утроения, учетверения и т.д. все они дают одинаковую амортизацию O(1) для вставки.

2. @matt: спасибо — я бы тоже так подумал, но не знал наверняка, поскольку не копал глубже 🙂

3. Это один длинный tl; dr, но 1 за поиск исходного кода браузера.

Ответ №3:

Еще один набор тестов производительности. Цикл выполняется над массивом из миллионов случайных чисел с пустым циклом.

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

В Safari и Firefox кэшированная длина неизменно была примерно в 2 раза быстрее, чем некэшированная версия.

Комментарии:

1. В Firefox 40 кэшированная и некэшированная версии имеют одинаковую производительность.

Ответ №4:

В этой статье исследуется автоматическое кэширование в V8 и Chrome, запрашивая у IRHydra сгенерированный код:

Как Гринч украл доступ к array.length Вячеслава Егорова

Он обнаружил, что при определенных условиях ручное кэширование .length фактически увеличивает накладные расходы, а не улучшает производительность!

Но в любом случае, такого рода микрооптимизация вряд ли приведет к какой-либо заметной выгоде для ваших пользователей. Для их пользы и для вашей, вместо этого сосредоточьтесь на коде, который понятен для чтения, и используйте в своем коде хорошие структуры данных и алгоритмы!

Избегайте преждевременной оптимизации: сосредоточьтесь на элегантном коде, пока не возникнет проблема с производительностью. Только после этого найдите узкое место с помощью профилирования, а затем оптимизируйте только эту часть кода.

Ответ №5:

Просто примечание:

В некоторых браузерах (я заметил это в Safari, IE и Opera) вы можете увеличить скорость, кэшируя длину внутри объявления цикла for:

 var j;
for (var i = 0, len = arr.length; i < len; i  ) {
  j = arr[i];
}
  

Я отредактировал приведенный выше тест jsperf @KooiInc, чтобы добавить этот случай.

Комментарии:

1. Из интереса, я только что провел этот тест в основных четырех браузерах (теперь включенных в диаграмму), и сохранение длины является довольно заметным плюсом в IE и Safari, но это не дает никакой пользы в Chrome и Firefox. Удивительно, но Firefox поразил другие браузеры. Неудивительно, что Safari был самым медленным в моем тестировании.

Ответ №6:

Будьте осторожны, чтобы не предполагать, что это верно для всех повторяющихся коллекций. Например, кэширование длины HTMLCollection выполняется на 65% быстрее в Chrome (версия 41) и на 35% быстрее в Firefox (версия 36).

http://jsperf.com/array-length-in-loop-dom

Комментарии:

1. Также важно отметить, что length не является статичным в HTMLCollection, оно будет настраиваться для отражения сопоставленных элементов.