Яка ефективність об’єктів / масивів у JavaScript? (спеціально для Google V8)


105

Продуктивність, пов’язана з масивами та об’єктами в JavaScript (особливо Google V8), було б дуже цікаво документувати. Я не знайду жодної вичерпної статті на цю тему ніде в Інтернеті.

Я розумію, що деякі Об'єкти використовують класи як основу структури даних. Якщо властивостей багато, це іноді трактується як хеш-таблиця?

Я також розумію, що масиви іноді трактуються як масиви C ++ (тобто швидка випадкова індексація, повільне видалення та зміна розміру). В іншому випадку до них трактуються більше як до об'єктів (швидка індексація, швидке вставлення / видалення, більше пам'яті). І, можливо, іноді вони зберігаються як пов'язані списки (тобто повільна випадкова індексація, швидке видалення / вставка на початку / в кінці)

Яка точна ефективність пошуку та маніпуляцій з об’єктами та маніпуляціями в 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 ... не так вже й погано.


2
Відвідайте jsperf.com та створіть тестові приклади.
Роб Ш

2
@RobW Тут грає більше, ніж прості тести, для яких потрібно знати, як працюють компілятори JIT і що робиться з даними. Якщо я знайду якийсь час, я додам відповідь, але, сподіваюсь, хтось інший встигне потрапити в нітну крупу. Також я хотів би просто залишити це посилання тут
Incognito

Речі JIT, про які я говорю, - це такі речі, як "форма" об'єкта або масиви з невизначеними значеннями між визначеними елементами, а також нещодавніші експерименти з функціями, що спеціалізуються на типі ... Методи, характерні для масиву, можуть залежати від використання як а також якщо прототипом було маніпульовано чи ні. Не існує такого поняття, як "знати" перейти на інший тип даних AFAIK.
інкогніто

1
Є представники Google, які обговорюють, як працює різний оптимізатор та внутрішня система. І як оптимізувати їх. (для ігор!) youtube.com/watch?v=XAqIpGU8ZZk
PicoCreator

Відповіді:


279

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

І в цьому сенсі ви можете побачити проблеми ефективності в цьому тестері тестування 50+ (це займе багато часу).

Крім того, як випливає з назви, він досліджує використання використання нативного пов'язаного характеру списку структури DOM.

(В даний час вниз, відбудується перебудова) Детальніше в моєму блозі щодо цього .

Резюме наведено нижче

  • V8 масив швидкий, ДУЖЕ Швидкий
  • Натискання / поп / зсув масиву ~ приблизно 20х + швидше, ніж будь-який об’єктний еквівалент.
  • Дивно Array.shift() швидко на ~ 6 разів повільніше, ніж спливаючий масив, але на ~ приблизно 100 разів швидше, ніж видалення атрибута об'єкта.
  • Кумедно, Array.push( data );швидше, ніж Array[nextIndex] = dataмайже на 20 (динамічний масив) до 10 (фіксований масив) разів.
  • Array.unshift(data) повільніше, як очікувалося, і ~ приблизно в 5 разів повільніше, ніж додавання нової властивості.
  • Видалення значення array[index] = nullшвидше, ніж його видаленняdelete array[index] (невизначене) в масиві на ~ приблизно 4х ++ швидше.
  • Напрочуд Знести значення в об'єкті є obj[attr] = null ~ приблизно в 2 рази повільніше, ніж просто видалити атрибут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. Також тест величезний, тому я ще повинен повністю проаналізувати та оцінити результати: будь ласка, відредагуйте його у =)

Оновлена ​​примітка (грудень 2012 р.): Представники Google розміщують відео на ютубах, що описують внутрішню роботу самого хрому (наприклад, коли він переходить із масиву пов'язаного списку на фіксований масив тощо) та способи їх оптимізації. Докладніше див. У розділі GDC 2012: Від консолі до Chrome .


2
Деякі з цих результатів виглядають дуже дивно. Наприклад, у Chrome масиві записи приблизно в 10 разів швидше, ніж читання, тоді як у Firefox - навпаки. Ви впевнені, що браузер JIT в деяких випадках не оптимізує весь ваш тест?
jjrv

1
@jjrv good gosh = O ви маєте рацію ... Я навіть оновлював кожен випадок запису, щоб бути поступово унікальним, щоб запобігти JIT ... І чесно, якщо тільки оптимізація JIT не є такою хорошою (у що мені важко повірити), це може бути просто випадок погано оптимізованого читання або сильно оптимізованого запису (записувати в негайний буфер?) ... що варто вивчити: lol
PicoCreator

2
просто хотів додати точну точку у відео дискусії про масиви: youtube.com/…
badunk

1
Сайт JsPerf більше не існує :(
JustGoscha,

1
@JustGoscha добре, THX для інформації: я виправив її назад, відтворивши її з кеша Google.
PicoCreator

5

На базовому рівні, який знаходиться у царинах JavaScript, властивості об’єктів є набагато складнішими об'єктами. Ви можете створити властивості за допомогою сетерів / геттерів, що відрізняються перелічуваністю, записуваністю та налаштованістю. Елемент у масиві неможливо налаштувати таким чином: він або існує, або його немає. На нижньому рівні двигуна це дозволяє значно оптимізувати організацію пам'яті, яка представляє структуру.

Щодо ідентифікації масиву з об’єкта (словника), двигуни JS завжди робили чіткі лінії між ними. Ось чому існує безліч статей про способи спроби зробити напівфальшивий об’єкт, схожий на масив, який веде себе як один, але дозволяє інші функціональні можливості. Причина цього поділу навіть існує в тому, що самі двигуни JS зберігають їх по-різному.

Властивості можна зберігати на об’єкті масиву, але це просто демонструє, як JavaScript наполягає на тому, щоб зробити все об'єктом. Індексовані значення масиву зберігаються по-різному від будь-яких властивостей, які ви вирішили встановити на об’єкті масиву, який представляє основні дані масиву.

Щоразу, коли ви використовуєте законний масив об’єкта та використовуєте один із стандартних методів маніпулювання цим масивом, ви збираєтеся потрапляти на базові дані масиву. Спеціально для V8 вони по суті такі ж, як і масив C ++, тому ці правила застосовуватимуться. Якщо ви чомусь працюєте з масивом, який двигун не в змозі з упевненістю визначити, це масив, то ви перебуваєте на набагато хиткішому просторі. З останніми версіями 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;
}

1

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

Ви можете дуже добре бачити цей ефект, це від 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;
    }
}

0

Під час роботи під node.js 0.10 (побудований на v8) я бачив використання процесора, яке здавалося надмірним для навантаження. Я простежив одну проблему продуктивності до функції, яка перевіряла наявність рядка в масиві. Тому я пройшов кілька тестів.

  • завантажено 90 822 господарів
  • конфігурація завантаження зайняла 0,087 секунд (масив)
  • конфігурація завантаження зайняла 0,152 секунди (об’єкт)

Завантаження 91k записів у масив (з валідацією та натисканням) швидше, ніж встановлення obj [key] = значення.

У наступному тесті я одночасно шукав кожне ім’я хосту у списку (ітерації 91k, для середнього часу пошуку):

  • пошук конфігурації зайняв 87,56 секунд (масив)
  • пошук конфігурації зайняв 0,21 секунди (об’єкт)

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.