[译] Javascript 中 Array.push 要比 Array.concat 快 945 倍! Бамбук

внешний интерфейс JavaScript Программа перевода самородков

Если вы хотите объединить массивы с тысячами элементов, используйтеarr1.push(...arr2)сопоставимыйarr1 = arr1.concat(arr2)сэкономить время. Если вы хотите работать еще быстрее, вы даже можете написать свою собственную функцию, которая выполняет объединенные массивы.

подожди... с.concatСколько времени занимает объединение 15000 массивов?

Недавно у нас был пользователь, жалующийся на то, что он используетUI-liciousПри тестировании их пользовательского интерфейса он был заметно медленнее. Обычно каждыйI.click I.fill I.seeКоманды, выполнение которых занимает около 1 секунды (постобработка, например создание скриншотов), теперь выполняются более 40 секунд, поэтому тесты, которые обычно выполнялись за 20 минут, теперь выполняются часами, что серьезно замедляет их процесс развертывания.

Я быстро установил таймер и заблокировал часть кода, которая вызывала замедление, но я был очень удивлен, когда нашел виновника:

arr1 = arr1.concat(arr2)

массив.concatметод.

Простые инструкции могут быть использованы для того, чтобы разрешить тестирование, напримерI.click("Login"), вместо использования селекторов CSS или XPATH, таких какI.click("#login-btn"), UI-licious использует динамический анализ кода (шаблоны) для анализа дерева DOM, чтобы определить, что тестировать и как тестировать веб-сайт, на основе семантики веб-сайта, свойств доступности и различных популярных, но нестандартных шаблонов. Эти.concatОперации используются для выравнивания дерева DOM для анализа, но когда дерево DOM очень большое и очень глубокое, производительность ужасна, это то, что случилось с нашими пользователями, когда они недавно обновили свое приложение, и эта волна обновлений также вызвала их Страница заметно раздута (это проблема с производительностью на их стороне, другая тема).

использовать.concatПотребовалось 6 секунд, чтобы объединить 15000 массивов со средним числом элементов 5.

Нани?

6 секунд...

Всего 15 000 массивов, а в среднем всего пять элементов?

Объем данных не велик.

Почему так медленно? Есть ли более быстрый способ объединить массивы?


Сравнительное сравнение

.push против .concat, объединить 10000 массивов из 10 элементов

Итак, я начал исследовать (я имею в виду гугление).concatСравнение с другими способами объединения массивов в Javascript.

Оказывается, самый быстрый способ объединить массивы — это использовать.pushметод, который может принимать n аргументов:

// 将 arr2 的内容压(push)入 arr1 中
arr1.push(arr2[0], arr2[1], arr2[3], ..., arr2[n])

// 由于我的数组大小不固定,我使用了 `apply` 方法
Array.prototype.push.apply(arr1, arr2)

Это быстрее по сравнению, прыжок.

Как быстро?

Я сам провел несколько тестов производительности, чтобы убедиться в этом. Вуаля, вот разница в исполнении в Chrome:

JsPerf - .push vs. .concat 10000 size-10 arrays (Chrome)

👉Ссылка на тесты на JsPerf

Merge имеет массив размером 10 10000 раз,.concatсоставляет 0,40 опс/сек (операций в секунду), а.pushСкорость 378 опс/сек. то естьpushСравниватьconcatПолный в 945 раз быстрее! Эта разница может не быть линейной, но на таком небольшом количестве данных уже очевидна.

В Firefox результаты выполнения следующие:

JsPerf - .push vs. .concat 10000 size-10 arrays (Firefox)

В целом, движок Firefox SpiderMonkey Javascript медленнее по сравнению с движком Chrome V8, но.pushпо-прежнему занимает первое место, болееconcatв 2260 раз быстрее.

Мы внесли вышеуказанные изменения в код, и это устранило все замедление.

.push против .concat, объединить 2 массива с 50000 элементами

Но что, если вместо 10 000 массивов по 10 элементов объединить два огромных массива по 50 000 элементов?

Вот результаты тестирования в Chrome:

JsPerf - .push vs. .concat 2 size-50000 arrays (chrome)

👉Ссылка на тесты на JsPerf

.pushеще лучше, чем.concatБыстрее, но на этот раз в 9 раз.

945x не сильно медленнее, но довольно медленно.


Более изящные операции расширения

если ты чувствуешьArray.prototype.push.apply(arr1, arr2)Это многословно, вы можете сделать простое преобразование, используя оператор распространения ES6:

arr1.push(...arr2)

Array.prototype.push.apply(arr1, arr2)а такжеarr1.push(...arr2)Разница в производительности между ними практически незначительна.


но почемуArray.concatтак медленно?

Это во многом связано с движком Javascript, и я не знаю точного ответа, поэтому я спросил своего друга@picocreator——GPU.jsСооснователь , потративший много времени на изучение исходного кода V8. Потому что у моего MacBook недостаточно памяти для запуска.concatОбъединить два массива длиной 50000,@picocreatorТакже одолжил мне детский игровой ПК, который он использовал для тестирования GPU.js для запуска тестов JsPerf.

Очевидно, что ответ во многом зависит от того, как они работают: при слиянии массивов.concatсоздает новый массив и.pushПросто изменяет первый массив. Эти дополнительные операции (добавление элементов первого массива в возвращаемый массив) выполняются медленно..concatКлюч к скорости.

Я: "Нани? Невозможно? И все? Но почему такой большой разрыв? Невозможно!" @picocreator: «Я не шучу, попробуйте написать собственные реализации .concat и .push, и вы все узнаете!»

Так что я попробовал в соответствии с тем, что он сказал, написал несколько методов реализации и добавил иlodashиз_.concatСравнение:

JsPerf - Various ways to merge arrays (Chrome)

👉Ссылка на тесты на JsPerf

Родная реализация 1

Давайте обсудим первый набор нативных реализаций:

.concatнативная реализация

// 创建结果数组
var arr3 = []

// 添加 arr1
for(var i = 0; i < arr1Length; i++){
  arr3[i] = arr1[i]
}

// 添加 arr2
for(var i = 0; i < arr2Length; i++){
  arr3[arr1Length + i] = arr2[i]
}

.pushнативная реализация

for(var i = 0; i < arr2Length; i++){
  arr1[arr1Length + i] = arr2[i]
}

Как видите, единственная разница между ними состоит в том,.pushПервый массив модифицируется непосредственно в реализации.

Результат обычного метода реализации:

  • .concat : 75 ops/sec
  • .push: 793 операции/сек (в 10 раз быстрее)

Результат нативной реализации метода 1:

  • .concat : 536 ops/sec
  • .push: 11 104 операций в секунду (в 20 раз быстрее)

Получается, что я сам это написалconcatа такжеpushБыстрее, чем их обычные реализации... но мы видим, что простое создание нового массива и копирование в него содержимого первого массива может значительно замедлить весь процесс.

Собственная реализация 2 (предварительно выделить окончательный размер массива)

Мы можем еще больше улучшить нативную реализацию, предварительно выделив размер массива перед добавлением элементов, что может иметь огромное значение.

с заранее выделенным.concatнативная реализация

// 创建结果数组并给它预先分配大小
var arr3 = Array(arr1Length + arr2Length)

// 添加 arr1
for(var i = 0; i < arr1Length; i++){
  arr3[i] = arr1[i]
}

// 添加 arr2
for(var i = 0; i < arr2Length; i++){
  arr3[arr1Length + i] = arr2[i]
}

с заранее выделенным.pushнативная реализация

// 预分配大小
arr1.length = arr1Length + arr2Length

// 将 arr2 的元素添加给 arr1
for(var i = 0; i < arr2Length; i++){
  arr1[arr1Length + i] = arr2[i]
}

Результат нативной реализации метода 1:

  • .concat : 536 ops/sec
  • .push: 11 104 операций в секунду (в 20 раз быстрее)

Результат нативной реализации метода 2:

  • .concat : 1,578 ops/sec
  • .push: 18 996 операций в секунду (в 12 раз быстрее)

Предварительное выделение размера конечного массива может повысить производительность каждого метода в 2-3 раза.

.pushмассив против..pushодин элемент

Что, если мы будем отправлять только один элемент за раз? это будет лучше, чемArray.prototype.push.apply(arr1, arr2)быстро?

for(var i = 0; i < arr2Length; i++){
  arr1.push(arr2[i])
}

результат

  • .pushВесь массив: 793 операции/сек.
  • .pushОдиночный элемент: 735 операций в секунду (медленно)

так.pushодин элемент, чем.pushВесь массив работает медленно, что также имеет смысл.

Вывод: почему.pushСравнивать.concatБыстрее

в общем,concatСравнивать.pushОсновная причина для замедления так много, что она создает новый массив, вам также необходимо скопировать дополнительный элемент массива на новый массив.

Теперь для меня еще одна загадка...

еще один фанат

Почему обычная реализация медленнее нативной? 🤔 Я снова@picocreatorИскать помощи.

Мы посмотрели на Лодаш_.concatреализации, хотите получить некоторую информацию о.concatПодсказки для обычных реализаций, так как они сопоставимы по производительности (lodash немного быстрее).

Факты доказали, что согласно.concatСпецификация общей реализации, этот метод перегружен и поддерживает два метода передачи параметров:

  1. Передайте n значений для добавления в качестве аргументов, например:[1,2].concat(3,4,5)
  2. Передайте массив для слияния в качестве параметра, например:[1,2].concat([3,4,5])

Вы даже можете написать:[1,2].concat(3,4,[5,6])

Lodash выполняет ту же перегрузку и поддерживает два способа передачи параметров: lodash помещает все параметры в массив, а затем сглаживает его. Так что имеет смысл, если вы передаете ему несколько массивов. Но когда вы передаете массив, который необходимо объединить, он не просто использует сам массив, а копирует его в новый массив, а затем сглаживает.

……Отлично……

Таким образом, вы определенно можете оптимизировать производительность. Вот почему вы хотите реализовать объединенные массивы самостоятельно.

Кроме того, это только я и@picocreatorОсновываясь на исходном коде Lodash и его немного устаревшем знании исходного кода V8,.concatПонимание того, как работает общая реализация движка.

Вы можете нажать в свободное времяздесьПрочтите исходный код lodash.


Дополнительные инструкции

  1. В наших тестах использовались только массивы, содержащие целые числа. Все мы знаем, что движки Javascript могут выполняться быстрее, используя массивы определенных типов. Если в массиве есть объекты, ожидается, что результат будет медленнее.

  2. Вот характеристики ПК, на котором выполнялся тест:

PC specs for the performance tests


Почему мы выполняем операции с такими большими массивами во время тестов, ориентированных на пользовательский интерфейс?

Uilicious Snippet dev.to test

Принцип работы механизма тестирования с учетом пользовательского интерфейса сканирует DOM-дерево целевого приложения, оценивая семантику, доступные атрибуты и другие распространенные шаблоны для определения целевых элементов и методов тестирования.

Таким образом, мы можем гарантировать, что тесты будут написаны так же просто, как:

// 跳转到 dev.to
I.goTo("https://dev.to")

// 在搜索框进行输入和搜索
I.fill("Search", "uilicious")
I.pressEnter()

// 我应该可以看见我自己和我的联合创始人
I.see("Shi Ling")
I.see("Eugene Cheah")

Селекторы CSS или XPATH не используются, что делает тесты более читабельными, менее чувствительными к изменениям в пользовательском интерфейсе и более простыми в обслуживании.

Примечание. Объявления общественных служб. Пожалуйста, держите DOM небольшим!

К сожалению, по мере того, как люди используют современные интерфейсные фреймворки для создания все более сложных и динамичных приложений, существует тенденция к тому, что деревья DOM становятся все больше и больше. Фреймворки — это палка о двух концах, которая позволяет нам развиваться быстрее, но люди часто забывают, какой багаж они добавляют. При изучении исходного кода различных веб-сайтов меня часто пугает количество элементов, которые существуют исключительно для того, чтобы оборачивать другие элементы.

Если вы хотите узнать, не слишком ли много узлов DOM на вашем веб-сайте, вы можете запуститьLighthouseПроверить.

Google Lighthouse

Согласно Google, оптимальное дерево DOM:

  • Менее 1500 узлов
  • глубина менее 32 уровней
  • Родительский узел имеет менее 60 дочерних узлов

Быстрый просмотр фида Dev.to показывает, что размер его дерева DOM довольно хорош:

  • Всего 941 узел
  • Максимальная глубина 14
  • Максимальное количество дочерних элементов — 49.

неплохо!

Если вы обнаружите ошибки в переводе или в других областях, требующих доработки, добро пожаловать наПрограмма перевода самородковВы также можете получить соответствующие бонусные баллы за доработку перевода и PR. начало статьиПостоянная ссылка на эту статьюЭто ссылка MarkDown этой статьи на GitHub.


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из ИнтернетаНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,товар,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.