Какова производительность объектов/массивов в JavaScript? (специально для Google V8)
Было бы очень интересно документировать производительность, связанную с массивами и объектами в JavaScript (особенно с Google V8). Я не нашел исчерпывающей статьи по этой теме в любом месте в Интернете.
Я понимаю, что некоторые объекты используют классы как свою базовую структуру данных. Если существует много свойств, это иногда рассматривается как хеш-таблица?
Я также понимаю, что массивы иногда обрабатываются как массивы С++ (т.е. быстрое случайное индексирование, медленное удаление и изменение размера). И в других случаях их обрабатывают скорее как объекты (быстрая индексация, быстрая вставка/удаление, большая память). И, возможно, иногда они хранятся в виде связанных списков (т.е. Медленная случайная индексация, быстрое удаление/вставка в начале/конце)
Какова точность выполнения запросов и манипуляций с массивами/объектами в JavaScript? (специально для Google V8)
В частности, что это за влияние производительности:
- Добавление свойства к объекту
- Удаление свойства из объекта
- Индексирование свойства в объекте
- Добавление элемента в массив
- Удаление элемента из массива
- Индексирование элемента в массиве
- Вызов Array.pop()
- Вызов Array.push()
- Вызов Array.shift()
- Вызов Array.unshift()
- Вызов Array.slice()
Также будут оценены любые статьи или ссылки для более подробной информации.:)
EDIT: Мне действительно интересно, как массивы JavaScript и объекты работают под капотом. Кроме того, в каком контексте двигатель V8 "знает" для "переключения" на другую структуру данных?
Например, предположим, что я создаю массив с...
var arr = [];
arr[10000000] = 20;
arr.push(21);
Что здесь происходит?
Или... как насчет этого...???
var arr = [];
//Add lots of items
for(var i = 0; i < 1000000; i++)
arr[i] = Math.random();
//Now I use it like a queue...
for(var i = 0; i < arr.length; i++)
{
var item = arr[i].shift();
//Do something with item...
}
Для обычных массивов производительность будет ужасной; тогда как, если был использован LinkedList... не так уж плохо.
Ответы
Ответ 1
ОБНОВЛЕНИЕ: Обратите внимание, что JSPref в настоящее время недоступен
(я сохранил копию тестового примера и обновляю ответ после того, как JSPref исправлен/найден преемник)
Хм... может быть, перегиб для ответа... но я создал набор тестов, чтобы изучить эти проблемы (и многое другое) (архивная копия).
И в этом смысле вы можете увидеть проблемы с производительностью в этом тесте 50+ test case (это займет много времени).
Также, как следует из его названия, в нем рассматривается использование использования родного списка связанных объектов структуры DOM.
(В настоящее время вниз, перестроен). Подробнее о моем блоге об этом.
Сводка как следует
- Массив V8 работает быстро, ОЧЕНЬ FAST
- Массив push/pop/shift ~ примерно на 20x + быстрее, чем любой эквивалент объекта.
- Удивительно
Array.shift()
быстро ~ примерно на 6 раз медленнее, чем массив, но примерно на 100 раз быстрее, чем удаление атрибута объекта.
- Интересно, что
Array.push( data );
быстрее, чем Array[nextIndex] = data
, почти 20 (динамический массив) до 10 (фиксированный массив) раз.
-
Array.unshift(data)
медленнее, как ожидалось, и ~ примерно на 5 раз медленнее, чем добавление нового свойства.
- Отбрасывание значения
array[index] = null
быстрее, чем удаление его delete array[index]
(undefined) в массиве примерно на ~ 4x ++ быстрее.
- Удивительно Отбрасывание значения в объекте
obj[attr] = null
~ примерно 2x медленнее, чем просто удаление атрибута delete obj[attr]
- Неудивительно, что средний массив
Array.splice(index,0,data)
медленный, очень медленный.
- Удивительно, что
Array.splice(index,1,data)
был оптимизирован (без изменения длины) и на 100 раз быстрее, чем просто сплайсинг Array.splice(index,0,data)
- неудивительно, что divLinkedList уступает массиву во всех секторах, кроме
dll.splice(index,1)
удаления (где он сломал тестовую систему).
- САМЫЙ БОЛЬШОЙ СЮРПРИЗ из всего этого [как указал jjrv], записи массива V8 немного быстрее, чем V8 читает = O
Примечание. Эти показатели применяются только к большому массиву/объектам, которые v8 не "полностью оптимизирует". Могут быть очень изолированные оптимизированные случаи производительности для размера массива/объекта меньше произвольного размера (24?). Более подробную информацию можно найти в нескольких видеороликах Google IO.
Примечание 2: Эти замечательные результаты производительности не разделяются между браузерами, особенно
*cough*
IE. Кроме того, тест огромен, поэтому я еще не полностью проанализировал и оценил результаты: отредактируйте его в =)
Обновлено примечание (dec 2012): У представителей Google есть видеоролики на youtubes, описывающие внутреннюю работу самого хрома (например, когда он переключается с массива связанных блоков на фиксированный массив и т.д.) и как оптимизировать их. Подробнее см. GDC 2012: от консоли до Chrome.
Обновленное примечание (feb 2013): спасибо @badunk, для обеспечения видеосвязи в точном месте
Обновлено примечание (июнь 2016): спасибо @Benedikt, относительно разности производительности массива в фиксированных/динамических массивах.
Ответ 2
На базовом уровне, который остается в рамках JavaScript, свойства на объектах намного сложнее. Вы можете создавать свойства с помощью сеттеров/геттеров с различной перечислимостью, возможностью записи и настраиваемостью. Элемент в массиве не может быть настроен таким образом: он либо существует, либо нет. На базовом уровне двигателя это обеспечивает гораздо большую оптимизацию с точки зрения организации памяти, представляющей структуру.
В терминах идентификации массива из объекта (словаря) двигатели JS всегда делали явные строки между ними. Вот почему существует множество статей о методах создания полуподобного объекта типа Array, который ведет себя как один, но позволяет использовать другие функции. Причина, по которой это разделение существует, заключается в том, что сами механизмы JS хранят эти два по-разному.
Свойства могут храниться в объекте массива, но это просто демонстрирует, как JavaScript настаивает на создании всего объекта. Индексированные значения в массиве хранятся иначе, чем любые свойства, которые вы решили установить для объекта массива, который представляет данные базового массива.
Всякий раз, когда вы используете законный объект массива и используете один из стандартных методов управления этим массивом, вы собираетесь поражать базовые данные массива. В V8, в частности, они по существу совпадают с массивом С++, поэтому эти правила будут применяться. Если по какой-то причине вы работаете с массивом, который движок не может определить с уверенностью, это массив, то вы на гораздо более шаткой почве. С недавними версиями V8 там больше места для работы. Например, можно создать класс с Array.prototype как прототип и все же получить эффективный доступ к различным методам манипуляции с собственными массивами. Но это недавнее изменение.
Здесь могут пригодиться конкретные ссылки на недавние изменения в манипулировании массивами:
Как немного лишний, здесь Array Pop и Array Push непосредственно из источника V8, оба реализованы в самой JS:
function ArrayPop() {
if (IS_NULL_OR_UNDEFINED(this) && !IS_UNDETECTABLE(this)) {
throw MakeTypeError("called_on_null_or_undefined",
["Array.prototype.pop"]);
}
var n = TO_UINT32(this.length);
if (n == 0) {
this.length = n;
return;
}
n--;
var value = this[n];
this.length = n;
delete this[n];
return value;
}
function ArrayPush() {
if (IS_NULL_OR_UNDEFINED(this) && !IS_UNDETECTABLE(this)) {
throw MakeTypeError("called_on_null_or_undefined",
["Array.prototype.push"]);
}
var n = TO_UINT32(this.length);
var m = %_ArgumentsLength();
for (var i = 0; i < m; i++) {
this[i+n] = %_Arguments(i);
}
this.length = n + m;
return this.length;
}
Ответ 3
При работе под node.js 0,10 (построенный на v8) я видел использование ЦП, которое казалось чрезмерным для рабочей нагрузки. Я проследил одну проблему производительности функции, которая проверяла наличие строки в массиве. Поэтому я провел несколько тестов.
- загружено 90 822 хостов.
- загрузка конфигурации заняла 0.087 секунд (массив)
- загрузка конфигурации заняла 0.152 секунды (объект)
Загрузка записей 91k в массив (с проверкой и нажатием) выполняется быстрее, чем установка obj [key] = value.
В следующем тесте я просмотрел каждое имя хоста в списке один раз (91k итераций, чтобы усреднить время поиска):
- search config занял 87,56 секунды (массив)
- search config занял 0.21 секунды (объект)
Приложением является Haraka (SMTP-сервер), и он загружает host_list один раз при запуске (и после изменений) и впоследствии выполняет этот поиск миллионы раз во время работы. Переключение на объект получило огромный выигрыш в производительности.
Ответ 4
Я хотел бы дополнить существующие ответы расследованием вопроса о том, как ведут себя реализации в отношении растущих массивов: если они реализуют их "обычным" способом, можно было бы увидеть много быстрых нажатий с редкими, вкрапленными медленными нажатиями, в какой точке реализация копирует внутреннее представление массива из одного буфера в более крупный.
Вы можете увидеть этот эффект очень красиво, это от Chrome:
16: 4ms
40: 8ms 2.5
76: 20ms 1.9
130: 31ms 1.7105263157894737
211: 14ms 1.623076923076923
332: 55ms 1.5734597156398105
514: 44ms 1.5481927710843373
787: 61ms 1.5311284046692606
1196: 138ms 1.5196950444726811
1810: 139ms 1.5133779264214047
2731: 299ms 1.5088397790055248
4112: 341ms 1.5056755767118273
6184: 681ms 1.5038910505836576
9292: 1324ms 1.5025873221216042
Несмотря на то, что каждый push профилирован, вывод содержит только те, которые занимают время выше определенного порогового значения. Для каждого теста я настроил порог, чтобы исключить все нажатия, которые, как представляется, представляют быстрые нажатия.
Итак, первое число представляет, какой элемент был вставлен (первая строка для 17-го элемента), вторая - сколько времени потребовалось (для многих массивов эталонный результат выполняется параллельно), а последним значением является деление первого числа на то, что было в первой строке.
Для Chrome исключены все строки с временем выполнения менее 2 мс.
Вы можете видеть, что Chrome увеличивает размер массива в 1,5, плюс некоторое смещение для учета небольших массивов.
Для Firefox это сила двух:
126: 284ms
254: 65ms 2.015873015873016
510: 28ms 2.0078740157480315
1022: 58ms 2.003921568627451
2046: 89ms 2.0019569471624266
4094: 191ms 2.0009775171065494
8190: 364ms 2.0004885197850513
Мне пришлось немного поменять порог в Firefox, поэтому мы начинаем С# 126.
В IE мы получаем комбинацию:
256: 11ms 256
512: 26ms 2
1024: 77ms 2
1708: 113ms 1.66796875
2848: 154ms 1.6674473067915691
4748: 423ms 1.6671348314606742
7916: 944ms 1.6672283066554338
Сначала он имеет силу двух, а затем переходит в силу пяти третей.
Таким образом, все распространенные реализации используют "обычный" способ для массивов (вместо того, чтобы сходить с ума с помощью канатов, например).
Здесь тестовый код и здесь скрипка в нем.
var arrayCount = 10000;
var dynamicArrays = [];
for(var j=0;j<arrayCount;j++)
dynamicArrays[j] = [];
var lastLongI = 1;
for(var i=0;i<10000;i++)
{
var before = Date.now();
for(var j=0;j<arrayCount;j++)
dynamicArrays[j][i] = i;
var span = Date.now() - before;
if (span > 10)
{
console.log(i + ": " + span + "ms" + " " + (i / lastLongI));
lastLongI = i;
}
}