Набір Javascript проти продуктивності масиву


87

Можливо, тому, що набори є відносно новими для Javascript, але я не зміг знайти статтю на StackO чи де-небудь ще, де говориться про різницю в продуктивності між ними в Javascript. Отже, яка різниця з точки зору продуктивності між ними? Зокрема, коли справа стосується видалення, додавання та ітерації.


1
Ви не можете використовувати їх як взаємозамінні. Тому дуже мало сенсу порівнювати їх.
zerkms

Ви говорите про порівняння між Setта []або {}?
виголошено

2
Додавання та ітерація не мають великої різниці, видалення та - найголовніше - пошук шукають різницю.
Бергі,


3
@ zerkms - суворо, масиви також не впорядковані, але їх використання індексу дозволяє поводитися з ними так, ніби вони є. ;-) Послідовність значень у наборі зберігається в порядку вставки.
RobG

Відповіді:


98

Добре, я протестував додавання, ітерацію та видалення елементів як з масиву, так і з набору. Я провів "малий" тест, використовуючи 10 000 елементів, і "великий" тест, використовуючи 100 000 елементів. Ось результати.

Додавання елементів до колекції

Здавалося б, .pushметод масиву приблизно в 4 рази швидший, ніж .addметод set, незалежно від кількості доданих елементів.

Ітерація та модифікація елементів у колекції

Для цієї частини тесту я використовував forцикл для ітерації масиву та for ofцикл для ітерації над набором. Знову ж таки, ітерація масиву була швидшою. Цього разу здавалося б, що це експоненційно, так як це зайняло вдвічі більше часу під час «малих» тестів і майже в чотири рази довше під час «великих» тестів.

Видалення елементів із колекції

Зараз тут стає цікаво. Я використовував комбінацію forциклу і .spliceдля видалення деяких елементів з масиву, а також використовував for ofі .deleteдля видалення деяких елементів із набору. Для "малих" тестів було приблизно втричі швидше видалити елементи з набору (2,6 мс проти 7,1 мс), але для "великого" тесту ситуація кардинально змінилася, коли для видалення елементів з масиву знадобилося 1955,1 мс, щоб видалити їх із набору, у 23 рази швидше пішло 83,6 мс.

Висновки

Для елементів 10k обидва тести виконувались порівнянно разів (масив: 16,6 мс, набір: 20,7 мс), але при роботі з елементами 100 тис. Набір був явним переможцем (масив: 1974,8 мс, набір: 83,6 мс), але лише через видалення операції. В іншому випадку масив був швидшим. Я не міг точно сказати, чому це так.

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

Код масиву:

var timer = function(name) {
  var start = new Date();
  return {
    stop: function() {
      var end = new Date();
      var time = end.getTime() - start.getTime();
      console.log('Timer:', name, 'finished in', time, 'ms');
    }
  }
};

var getRandom = function(min, max) {
  return Math.random() * (max - min) + min;
};

var lastNames = ['SMITH', 'JOHNSON', 'WILLIAMS', 'JONES', 'BROWN', 'DAVIS', 'MILLER', 'WILSON', 'MOORE', 'TAYLOR', 'ANDERSON', 'THOMAS'];

var genLastName = function() {
  var index = Math.round(getRandom(0, lastNames.length - 1));
  return lastNames[index];
};

var sex = ["Male", "Female"];

var genSex = function() {
  var index = Math.round(getRandom(0, sex.length - 1));
  return sex[index];
};

var Person = function() {
  this.name = genLastName();
  this.age = Math.round(getRandom(0, 100))
  this.sex = "Male"
};

var genPersons = function() {
  for (var i = 0; i < 100000; i++)
    personArray.push(new Person());
};

var changeSex = function() {
  for (var i = 0; i < personArray.length; i++) {
    personArray[i].sex = genSex();
  }
};

var deleteMale = function() {
  for (var i = 0; i < personArray.length; i++) {
    if (personArray[i].sex === "Male") {
      personArray.splice(i, 1)
      i--
    }
  }
};

var t = timer("Array");

var personArray = [];

genPersons();

changeSex();

deleteMale();

t.stop();

console.log("Done! There are " + personArray.length + " persons.")

Встановити код:

var timer = function(name) {
    var start = new Date();
    return {
        stop: function() {
            var end  = new Date();
            var time = end.getTime() - start.getTime();
            console.log('Timer:', name, 'finished in', time, 'ms');
        }
    }
};

var getRandom = function (min, max) {
  return Math.random() * (max - min) + min;
};

var lastNames = ['SMITH','JOHNSON','WILLIAMS','JONES','BROWN','DAVIS','MILLER','WILSON','MOORE','TAYLOR','ANDERSON','THOMAS'];

var genLastName = function() {
    var index = Math.round(getRandom(0, lastNames.length - 1));
    return lastNames[index];
};

var sex = ["Male", "Female"];

var genSex = function() {
    var index = Math.round(getRandom(0, sex.length - 1));
    return sex[index];
};

var Person = function() {
	this.name = genLastName();
	this.age = Math.round(getRandom(0,100))
	this.sex = "Male"
};

var genPersons = function() {
for (var i = 0; i < 100000; i++)
	personSet.add(new Person());
};

var changeSex = function() {
	for (var key of personSet) {
		key.sex = genSex();
	}
};

var deleteMale = function() {
	for (var key of personSet) {
		if (key.sex === "Male") {
			personSet.delete(key)
		}
	}
};

var t = timer("Set");

var personSet = new Set();

genPersons();

changeSex();

deleteMale();

t.stop();

console.log("Done! There are " + personSet.size + " persons.")


1
Майте на увазі, значення набору за замовчуванням унікальні. Отже, де як [1,1,1,1,1,1]для масиву буде довжина 6, набір матиме розмір 1. Схоже, ваш код може насправді генерувати набори дико різних розмірів, ніж розмір 100 000 елементів на кожному запуску, завдяки цій рисі наборів. Ви, мабуть, ніколи не помічали, оскільки розмір набору ви показуєте лише після запуску всього сценарію.
KyleFarris

6
@KyleFarris Якщо я не помиляюсь, це було б правдою, якби в наборі були дублікати, як у вашому прикладі [1, 1, 1, 1, 1], але оскільки кожен елемент набору насправді є об’єктом з різними властивостями, включаючи ім’я та прізвище, випадково сформовані зі списку з сотень можливих імен, випадково згенерованого віку, випадково сформованого статі та інших випадково згенерованих атрибутів ... шанси мати два однакові об'єкти в наборах незначні.
snowfrogdev

3
Насправді, ви маєте рацію в цьому випадку, оскільки здається, що набори насправді не відрізняються від об'єктів у наборі. Отже, ви могли б навіть мати такий самий точний об’єкт {foo: 'bar'}10000x у наборі, і він мав би розмір 10000. Те саме стосується масивів. Здається, це унікально лише зі скалярними значеннями (рядки, числа, булеві значення тощо).
KyleFarris

12
Ви можете мати однаковий точний вміст об’єкта {foo: 'bar'} багато разів у Наборі, але не той самий об’єкт (посилання). Варто вказати на тонку різницю ІМО
SimpleVar

14
Ви забули міру, найважливішу причину використання набору, пошук 0 (1). hasпроти IndexOf.
Магнус,

65

СПОСТЕРЕЖЕННЯ :

  • Набір операцій можна розуміти як знімки в потоці виконання.
  • Ми не до остаточної заміни.
  • Елементи класу Set не мають доступних індексів.
  • Клас Set - це доповнення до класу Array , корисне в тих сценаріях, коли нам потрібно зберігати колекцію, до якої застосовуватимуться основні операції додавання, видалення, перевірки та ітерації.

Я ділюсь деяким тестом продуктивності. Спробуйте відкрити консоль і скопіювати код нижче.

Створення масиву (125000)

var n = 125000;
var arr = Array.apply( null, Array( n ) ).map( ( x, i ) => i );
console.info( arr.length ); // 125000

1. Розташування індексу

Ми порівняли метод has з Set із Array indexOf:

Array / indexOf (0,281ms) | Встановити / має (0,053 мс)

// Helpers
var checkArr = ( arr, item ) => arr.indexOf( item ) !== -1;
var checkSet = ( set, item ) => set.has( item );

// Vars
var set, result;

console.time( 'timeTest' );
result = checkArr( arr, 123123 );
console.timeEnd( 'timeTest' );

set = new Set( arr );

console.time( 'timeTest' );
checkSet( set, 123123 );
console.timeEnd( 'timeTest' );

2. Додавання нового елемента

Ми порівнюємо методи add та push об'єктів Set та Array відповідно:

Масив / натискання (1,612 мс) | Встановити / додати (0,006 мс)

console.time( 'timeTest' );
arr.push( n + 1 );
console.timeEnd( 'timeTest' );

set = new Set( arr );

console.time( 'timeTest' );
set.add( n + 1 );
console.timeEnd( 'timeTest' );

console.info( arr.length ); // 125001
console.info( set.size ); // 125001

3. Видалення елемента

Видаляючи елементи, ми повинні пам’ятати, що Array та Set не запускаються за однакових умов. Масив не має власного методу, тому необхідна зовнішня функція.

Array / deleteFromArr (0,356ms) | Встановити / видалити (0,019 мс)

var deleteFromArr = ( arr, item ) => {
    var i = arr.indexOf( item );
    i !== -1 && arr.splice( i, 1 );
};

console.time( 'timeTest' );
deleteFromArr( arr, 123123 );
console.timeEnd( 'timeTest' );

set = new Set( arr );

console.time( 'timeTest' );
set.delete( 123123 );
console.timeEnd( 'timeTest' );

Повну статтю читайте тут


4
Array.indexOf має бути Array.includes, щоб вони були еквівалентними. Я отримую дуже різні цифри у Firefox.
kagronick

2
Мене зацікавить порівняння Object.includes vs Set.has ...
Леопольд Крістьянсон,

1
@LeopoldKristjansson Я не писав тест порівняння, але ми проводили таймінги на виробничому майданчику з масивами з 24 тис. Елементів і перехід з Array.includes на Set.has надзвичайно підвищив продуктивність!
sedot

3

Я зауважую, що набір завжди краще з урахуванням двох підводних каменів для великих масивів:

а) Створення наборів із масивів повинно виконуватися у forциклі із заздалегідь закріпленою довжиною.

повільний (наприклад, 18 мс) new Set(largeArray)

швидко (наприклад, 6 мс) const SET = new Set(); const L = largeArray.length; for(var i = 0; i<L; i++) { SET.add(largeArray[i]) }

б) Ітерацію можна зробити так само, оскільки це також швидше, ніж for ofцикл ...

Див. Https://jsfiddle.net/0j2gkae7/5/

для реального порівняння життя до difference(), intersection(), union()і uniq()(+ їх iteratee компаньйонів і т.д.) з 40.000 елементами


3

Знімок екрана порівняльної ітераціїЩо стосується ітераційної частини вашого запитання, я нещодавно провів цей тест і виявив, що Сет значно перевершив масив із 10000 елементів (приблизно в 10 разів операції можуть відбуватися за той самий проміжок часу). І в залежності від браузера або бити, або програти Object.hasOwnProperty в подібному для подібного тесту.

І Set, і Object мають метод "має", який виконується в тому, що здається амортизованим до O (1), але залежно від реалізації браузера одна операція може зайняти більше часу або швидше. Здається, що більшість браузерів реалізують ключ в Object швидше, ніж Set.has (). Навіть Object.hasOwnProperty, який включає додаткову перевірку ключа, приблизно на 5% швидший, ніж Set.has (), принаймні для мене в Chrome v86.

https://jsperf.com/set-has-vs-object-hasownproperty-vs-array-includes/1

Оновлення: 11.11.2020: https://jsbench.me/irkhdxnoqa/2

Якщо ви хочете запустити власні тести з різними браузерами / середовищами.


Подібним чином я додаю еталон для додавання елементів до масиву проти набору та видалення.


4
Будь ласка, не використовуйте посилання у своїх відповідях (якщо вони не пов’язані з офіційними бібліотеками), оскільки ці посилання можуть бути порушені - як це трапилось у вашому випадку. Ви посилаєтесь на 404.
Гіл Епштейн,

Я використав посилання, але також скопіював вихідні дані, коли це було доступно. Прикро, що вони так швидко змінили свою стратегію зв’язування.
Заргольд

Оновив публікацію зараз скріншотом та новим веб-сайтом про продуктивність JS: jsbench.me
Zargold

-5
console.time("set")
var s = new Set()
for(var i = 0; i < 10000; i++)
  s.add(Math.random())
s.forEach(function(e){
  s.delete(e)
})
console.timeEnd("set")
console.time("array")
var s = new Array()
for(var i = 0; i < 10000; i++)
  s.push(Math.random())
s.forEach(function(e,i){
  s.splice(i)
})
console.timeEnd("array")

Ці три операції над предметами 10K дали мені:

set: 7.787ms
array: 2.388ms

@Bergi, це те, що я спочатку також думав, але це так.
zerkms

1
@zerkms: Визначте "робота" :-) Так, масив буде порожнім після forEach, але, мабуть, не так, як ви очікували. Якщо хтось хоче порівнянної поведінки, це повинно бути s.forEach(function(e) { s.clear(); })також.
Бергі,

1
Ну, він робить щось, тільки не те, що призначено: він видаляє всі елементи між індексом i та кінцем. Це не порівняно з тим, що deleteробить на знімальному майданчику.
trincot

@Bergi, так, він видаляє все лише за 2 ітерації. Моє ліжко.
zerkms

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