Какова производительность объектов/массивов в JavaScript? (специально для Google V8)


производительность, связанная с массивами и объектами в JavaScript (особенно Google V8) было бы очень интересно документировать. Я не нахожу исчерпывающей статьи на эту тему нигде в Интернете.

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

Я также понимаю, что массивы иногда обрабатываются как массивы C++ (т. е. быстрая случайная индексация, медленная удаление и изменение). И, в других случаях, они обрабатываются больше как объекты (быстрая индексация, быстрая вставка/удаление, больше памяти). И, возможно, иногда они хранятся в виде связанных списков (т. е. медленное случайное индексирование, быстрое удаление/вставка в начале/конце)

какова точная производительность извлечения массива / объекта и манипуляций в JavaScript? (специально для Google V8)

более конкретно, какое это влияние на производительность оф:

  • добавление свойства к объекту
  • удаление свойства из объекта
  • индексирование свойства в объекте
  • добавление элемента в массив
  • удаление элемента из массива
  • индексирование элемента в массиве
  • Вызове Array.pop ()
  • Вызове Array.толкать()
  • Вызове Array.shift ()
  • Вызове Array.unshift ()
  • вызов Матрица.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... не так уж и плохо.

4 99

4 ответа:

обновление: обратите внимание, что JSPref в настоящее время не работает

(я сохранил копию тестового случая и обновлю ответ, как только jspref будет исправлен / найден преемник)


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

и в этом смысле, вы можете увидеть проблемы с производительностью в этом тестере 50 + test case (это займет много времени).

кроме того, как следует из названия, он исследует использование собственного связанного списка структуры DOM.

(в настоящее время вниз, перестроен в процессе) более подробная информация об этом в моем блоге.

резюме выглядит следующим образом

  • V8 массив быстро, очень быстро
  • массив push / pop / shift ~прибл 20x + быстрее, чем любой эквивалент объекта.
  • удивительно Array.shift() быстро ~приблизительно в 6 раз медленнее, чем массив pop, но ~приблизительно в 100 раз быстрее, чем удаление атрибута объекта.
  • забавно, Array.push( data ); быстрее Array[nextIndex] = data почти в 20 (динамический массив) до 10 (фиксированный массив) раз.
  • Array.unshift(data) медленнее, как и ожидалось, и ~приблизительно в 5 раз медленнее, чем добавление нового свойства.
  • обнуление значения array[index] = null быстрее, чем удалить его delete array[index] (неопределенный) в массиве на ~приблизительно 4x++ быстрее.
  • удивительно обнуление значения в объекте obj[attr] = null ~приблизительно в 2 раза медленнее, чем просто удаление атрибута delete obj[attr]
  • неудивительно, что mid array 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. Также Тест огромен, поэтому мне еще предстоит полностью проанализировать и оценить результаты : пожалуйста, отредактируйте его в=)

Обновлено Примечание (декабрь 2012): у представителей Google есть видео на youtubes, описывающие внутреннюю работу самого chrome (например, когда он переключается с массива linkedlist на фиксированный массив и т. д.) и как оптимизировать их. Смотрите GDC 2012: от консоли до Chrome дополнительные.

Обновлено Примечание (февраль 2013): Thx @badunk, для предоставления видеосвязи в точном месте

Обновлено Примечание (июнь 2016): спасибо @Бенедикт, относительно выбора производительности разница в фиксированные / динамические массивы.

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

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

свойства могут храниться в объекте массива, но это просто демонстрирует, как JavaScript настаивает на создании все является объектом. Индексированные значения в массиве хранятся иначе, чем любые свойства, которые вы решили установить для объекта массива, представляющего базовые данные массива.

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

конкретные ссылки на последние изменения в манипуляции с массивами могут пригодиться здесь:

Как немного больше, вот 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;
}

во время работы в узле.js 0.10 (построенный на v8) я видел использование ЦП, которое казалось чрезмерным для рабочей нагрузки. Я проследил одну проблему производительности до функции, которая проверяла наличие строки в массиве. Поэтому я провел несколько тестов.

  • загружено 90,822 хостов
  • загрузка конфигурации заняла 0,087 секунды (массив)
  • загрузка конфигурации заняла 0,152 секунды (объект)

загрузка 91k записей в массив (с validate & push) является быстрее, чем установка obj[key]=value.

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

  • поиск конфигурации занял 87.56 секунд (массив)
  • поиск конфигурации занял 0,21 секунды (объект)

приложение здесь Haraka (SMTP-сервер), и он загружает host_list один раз при запуске (и после изменений), а затем выполняет этот поиск миллионы раз во время операция. Переход на объект был огромным выигрышем в производительности.

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

вы можете увидеть этот эффект очень хорошо, это от 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

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

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

все строки, которые имеют время выполнения менее 2 мс, исключаются для Chrome.

вы можете видеть, что 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;
    }
}