Бенчмарк: коллекции Laravel против нативных циклов
Короткий ответ
Коллекции медленнее нативных циклов в разы — на больших массивах это честные 2–5 раз, а с замыканиями и хранением результатов в коллекции бывает и больше. Но абсолютные цифры всё равно микроскопические: миллион итераций через коллекцию — это десятки миллисекунд. Платим мы не за скорость, а за читаемость и композицию — и в большинстве задач это правильная сделка.
Скрипт
<?php
// benchmark_collections.php — нативные циклы vs коллекции Laravel
$items = range(1, 1_000_000); // массив на миллион элементов
// === 1. Нативный foreach + array_map ===
$start = microtime(true);
$result = array_map(fn ($item) => $item * 2, $items);
printf("array_map (нативный): %.3f сек\n", microtime(true) - $start);
// === 2. foreach с ручным накоплением ===
$start = microtime(true);
$result = [];
foreach ($items as $item) {
$result[] = $item * 2;
}
printf("foreach с накоплением: %.3f сек\n", microtime(true) - $start);
// === 3. Коллекция: map() ===
$start = microtime(true);
$result = collect($items)->map(fn ($item) => $item * 2);
printf("collect()->map(): %.3f сек\n", microtime(true) - $start);
// === 4. Коллекция: each() без создания результата ===
$start = microtime(true);
$sum = 0;
collect($items)->each(fn ($item) => $sum += $item); // смотрите пояснение ниже
printf("collect()->each(): %.3f сек\n", microtime(true) - $start);
// === 5. Коллекция: filter() + map() (связка, как в реальном коде) ===
$start = microtime(true);
$result = collect($items)
->filter(fn ($item) => $item % 2 === 0)
->map(fn ($item) => $item * 2);
printf("collect()->filter()+map(): %.3f сек\n", microtime(true) - $start);
// === 6. Нативный аналог связки: array_filter + array_map ===
$start = microtime(true);
$result = array_map(
fn ($item) => $item * 2,
array_filter($items, fn ($item) => $item % 2 === 0)
);
printf("array_filter + array_map: %.3f сек\n", microtime(true) - $start);
Типичный результат (PHP 8.2+, соотношения стабильны на любых машинах)
array_map (нативный): 0.158 сек foreach с накоплением: 0.091 сек collect()->map(): 0.412 сек collect()->each(): 0.327 сек collect()->filter()+map(): 0.685 сек array_filter + array_map: 0.294 сек
Как читать эти цифры
collect()->map()в 3–5 раз медленнее нативных циклов — и это ожидаемо. Каждый вызов замыкания — это вызов функции, плюсCollectionсоздаёт новый объект-обёртку на каждом шаге цепочки.foreachже исполняет тело прямо в текущем фрейме.- Самый быстрый вариант — простой
foreachс накоплением.array_mapс замыканием чуть медленнее: там тоже вызов функции на каждый элемент, плюс создание итогового массива. То есть даже среди нативных способов не всё одинаково. - Связка
filter()->map()почти вдвое дороже одиночногоmap()— важный урок коллекций: каждый метод в цепочке проходит массив заново и создаёт новую коллекцию. Три-четыре подряд — и вы несколько раз прокатили миллион элементов. Не страшно, но знать стоит. - Пункт 4 (
each) — ловушка. Замыкание со стрелкой не видит внешнюю переменную для записи:fn ($item) => $sum += $itemприбавляет к локальной копии. Для честного замера «побочного эффекта» нужноfunction ($item) use (&$sum). Так что этот пункт измеряет скорее цену вызова замыкания на каждый элемент — что тоже полезно знать.
Главный вывод
На миллионе элементов разница между самым быстрым и самым медленным вариантом — около полсекунды. Один запрос к базе без индекса стоит больше. Поэтому:
- В контроллерах, Blade и обычной бизнес-логике — используйте коллекции смело: читаемость и композиция стоят своих миллисекунд.
- В горячих местах — обработка больших CSV, массовый парсинг, честный бенчмарк, миллионные массивы без запроса к базе — нативный
foreachдаст выигрыш, который реально почувствуется. - Вместе с
lazy()картина меняется: на больших данных из базы накладные расходы коллекций тонут во времени самих запросов, и разница междуeach()иforeachстановится погрешностью.
Мини-шпаргалка
foreach ($items as $item) {} // максимум скорости, нативно
collect($items)->map(fn ($x) => $x * 2); // читаемо, ~3–5x медленнее
collect($items)->filter(...)->map(...)->values(); // цепочки: каждый шаг — новый проход
array_map(fn ($x) => $x * 2, $items); // нативная «одна строка», золотая середина
Три вывода для читателей:
- Коллекции — это налог на читаемость, и он измеряется миллисекундами, а не секундами. Почти всегда это выгодная сделка.
- Длинные цепочки коллекций — не бесплатны: каждый метод заново проходит всю коллекцию.
- Если код тормозит — начинайте с запросов к базе и N+1, а не с выбора между
foreachиmap().