PHP производительность array_merge в цикле
Если использовать array_merge в цикле, то phpStrom + плагин Php Inspections (EA Extended) даёт подсказку: [EA] 'array_merge(...)' is used in a loop and is a resources greedy construction.
Стало интересно, насколько сильно влияет на произовдительность использование array_merge в цикле и какие есть альтернативные решения.
Рассмотрим пример. Имеется список товаров следующей структуры:
$products = [
['id' => 1, 'tags' => ['tag_1', 'tag_2', 'tag_3']],
['id' => 2, 'tags' => ['tag_1', 'tag_2', 'tag_3']],
];
Нужно получить список всех тегов.
В экспериментах решил проверить скорость скрипта при количестве в 10, 250, 500, 1000, 2000, 5000, 10000, 50000 товаров, и также изменять количество тегов внутри каждого товара: 1, 3, 6 тегов.
Объединение массивов
Выделил следующие способы объединить два и более массивов в один:
- array_merge
- Оператор …
- Оператор +
- array_replace
- foreach
Рассмотрим каждый из способов с примерами кода.
Использование array_merge
$tags = [];
foreach($products as $product) {
$tags = array_merge($tags, $product['tags']);
}
https://gist.github.com/zualex/39b3dfe9503b69468307f420089d0c42
Распаковка массива через оператор …
$tags = [];
foreach($products as $product) {
$tags[] = $product['tags'];
}
$tags = array_merge(...$tags);
https://gist.github.com/zualex/c1bd85a1c2fee5b4c362953211d976ea
Использование оператора +
При использовании оператора + если совпадают индексы, то в результирующем массиве будут только элементы из массива слева от оператора. Чтобы этого избежать потребуется переделать структуру хранения товаров, чтобы в списке тегов был уникальный индекс.
$products = [
['id' => 1, 'tags' => ['tag_1' => tag_1', 'tag_2' => 'tag_2', 'tag_3' => 'tag_3']],
['id' => 2, 'tags' => ['tag_1' => tag_1', 'tag_2' => 'tag_2', 'tag_3' => 'tag_3']],
];
$tags = [];
foreach($products as $product) {
$tags += $product['tags'];
}
https://gist.github.com/zualex/902a6f9e9d1f23a7da7cdd3bebf72812
Использование array_replace
В данном примере тоже потребуется использовать структуру хранения товаров, как при использовании оператора +.
$tags = [];
foreach($products as $product) {
$tags = array_replace($tags, $product['tags']);
}
https://gist.github.com/zualex/b656a5f12992f96edaacf1bb22cd0bf7
Использование foreach
$tags = [];
foreach($products as $product) {
foreach($product['tags'] as $tag) {
$tags[] = $tag;
}
}
https://gist.github.com/zualex/0d101fb8b48701352117cfc253be2762
Анализ производительности
Результаты прогонов в этой таблице: https://docs.google.com/spreadsheets/d/1Va7pg5iaPbXMxkbcQ5sOTco0d316IUzLCOcHq1CrzRo/edit?usp=sharing
Эксперименты показали, что для PHP 8.0 и 7.4 нет существенной разницы между версиями. Из-за этого примеры будут для PHP 7.4.
Для 6 тегов нет расчетов из-за memory limit во время прогона тестов.
Как видно из графиков, ...
, +
, foreach
имеют линейную зависимость.
array_merge
, array_replace
- видна квадратичная зависимость. В экспериментах данные имеются только для 10000.
Сравним вместе
Теперь попробуем совместить графики, чтобы увидеть общую картину. Сравнивать буду для PHP 7.4 с использованием 3-х тегов.
График для кол-во от 10 до 500 товаров.
Как видно, уже начиная от 250, а может и ранее (в экспериментах делал тесты для 10, 250, 500 товаров), array_merge
и array_replace
отрабатывают дольше всех.
Теперь если посмотреть для 50000 товаров.
array_merge
и array_replace
в рамках теста, смог замерить только для 10000 товаров. Разница существенна, на порядки.
А что с памятью?
С памятью всё ок, все подходы практически одинаково используют память.
Самый быстрый подход
В данных эксперимента самым быстрым оказался обычный foreach, но разница столь не существенна, что думаю не стоит делать разницы между ...
, +
и foreach
.
А если без цикла?
Для полноты картины посмотрим на те же подходы, если без цикла объединить 3 больших массива по 100000 элементов в каждом.
$big1 = range(0, 100000);
$big2 = array_fill(100000, 100000, 'string');
$big3 = range(200000, 100000);
Без цикла array_merge
$result = array_merge($big1, $big2, $big3);
https://gist.github.com/zualex/a9f536006c1a24f7c4d4888077dc9fc0
Без цикла Распаковка массива через оператор …
$result = [...$big1, ...$big2, ...$big3];
https://gist.github.com/zualex/a35cd71c38480658a1bbaf6559c4c2fe
Без цикла оператор +
$result = $big1 + $big2 + $big3;
https://gist.github.com/zualex/6e6d9d584cc979b59da517f9078e6559
Без цикла array_replace
$result = array_replace($big1, $big2, $big3);
https://gist.github.com/zualex/4e770098f40f2f101138dda7251f32ca
Без цикла foreach
$result = [];
foreach ($big1 as $key => $value) {
$result[$key] = $value;
}
foreach ($big2 as $key => $value) {
$result[$key] = $value;
}
foreach ($big3 as $key => $value) {
$result[$key] = $value;
}
https://gist.github.com/zualex/4e770098f40f2f101138dda7251f32ca
Анализ данных без циклов
Скрость
Память
Как видно, array_merge
быстрее всех справляется с объединением больших массивов без использования циклов. По памяти Чуть экномичнее foreach
и array_replace
Вывод
array_merge
и array_replace
не нужно использовать в циклах, даже в маленьких. При решении данной задачи лучше выбрать ...
, +
или foreach
. Но стоит не забывать отличия в работе array_merge
и +
.
Но если стоит задача объединить массивы, когда нет циклов, то лучше использовать array_merge
.