for против foreach: что быстрее — и почему почти всегда всё равно
Короткий ответ
foreach быстрее на обходе массивов в подавляющем большинстве случаев. И дело не в магии, а в том, как эти два цикла устроены внутри.
Но — спойлер из конца статьи — на реальных проектах вы почти никогда не заметите разницы. Разбираемся, почему.
Как устроены оба цикла
for — цикл «по индексам»
$length = count($items); // важно: вынесли за пределы цикла!for ($i = 0; $i < $length; $i++) {echo $items[$i];}
На каждой итерации PHP делает три вещи: инкремент $i, сравнение $i < $length и обращение $items[$i] — двойное разыменование (сначала массив, потом элемент по ключу).
И вот классическая ловушка, которая убивает производительность for быстрее всего:
// ПЛОХО: count() вызывается на КАЖДОЙ итерацииfor ($i = 0; $i < count($items); $i++) {echo $items[$i];}
count() внутри условия — это вызов функции на каждом шаге. На массиве в миллион элементов — миллион лишних вызовов. Именно поэтому советы «for медленный» исторически отчасти справедливы: не сам for медленный, а так его пишут.
foreach — цикл «по элементам»
foreach ($items as $item) {
echo $item;
}
У foreach нет счётчика, нет сравнения условия, нет разыменования по ключу. Он работает напрямую со внутренним итератором массива — хеш-таблицы Zend Engine — и просто переходит к следующему элементу. Синтаксически вы просите меньше работы, и PHP её делает меньше.
Замер: проверим на практике
Тот же принцип, что и в прошлой заметке про lazy() — честный бенчмарк, который можно запустить в tinker:
<?php
// benchmark_loops.php — for vs foreach
$items = range(1, 1_000_000); // массив на миллион элементов
// === 1. for с count() внутри условия — так писать НЕ надо ===
$start = microtime(true);
$sum = 0;
for ($i = 0; $i < count($items); $i++) {
$sum += $items[$i];
}
printf("for с count() в условии: %.3f сек\n", microtime(true) - $start);
// === 2. for с вынесенным count() — «правильный» for ===
$start = microtime(true);
$sum = 0;
$length = count($items);
for ($i = 0; $i < $length; $i++) {
$sum += $items[$i];
}
printf("for с вынесенным count(): %.3f сек\n", microtime(true) - $start);
// === 3. foreach по значению ===
$start = microtime(true);
$sum = 0;
foreach ($items as $item) {
$sum += $item;
}
printf("foreach по значению: %.3f сек\n", microtime(true) - $start);
// === 4. foreach со ссылкой — заметьте разницу ===
$start = microtime(true);
$sum = 0;
foreach ($items as &$item) {
$sum += $item;
}
unset($item); // обязательно!
printf("foreach по ссылке: %.3f сек\n", microtime(true) - $start);
Типичный результат на PHP 8.2+ (конкретные цифры зависят от машины, но соотношения стабильно):
for с count() в условии: 0.281 сек for с вынесенным count(): 0.142 сек foreach по значению: 0.126 сек foreach по ссылке: 0.131 сек
Что видно:
- Правильный
forпочти не отличается отforeach. Разница в 10–15% — это микроскопия на фоне реальных задач. forсcount()в условии — в два раза медленнее. Это и есть главный источник мифа «for тормозит».foreachпо ссылке чуть дороже по значению — PHP создаёт ссылочную пару. Но нюанс тоньше, о нём ниже.
Один нюанс про foreach по ссылке
Если массив создаётся «на лету» (результат функции), foreach ($items as &$item) заставит PHP сначала материализовать весь результат. Для больших данных это иногда заметнее, чем разница между циклами. Плюс после ссылочного foreach обязателен unset($item) — иначе последующая запись в $item тихо изменит последний элемент массива. Классическая ошибка копипасты.
А если использовать foreach по ключам?
foreach ($items as $i => $item) {
// нужен и индекс, и значение
}
Сюда же относится частый совет «замени for на foreach ($array as $i => $v)». По скорости это паритет с правильным for — но читается такой цикл проще и работает ещё и для коллекций, где индексы могут быть строковыми. Так что если в for вам нужен только порядковый номер — да, as $i => $item часто удобнее.
Почему на реальном проекте это не важно
Вот в чём подвох всех этих микросекунд. Цикл на миллионе элементов разница между for и foreach экономит порядка 0.02 секунды. А теперь вспомним, что действительно съедает время в типичном Laravel-приложении:
- запрос к базе без индекса — 50–500 мс;
- N+1 в цикле по моделям — сотни мс и растёт с данными;
- сериализация и рендер — десятки мс.
Один неоптимизированный SQL-запрос стоит дороже, чем разница между циклами на миллионе итераций. Цикл for вместо foreach вы спасаете только если внутри него — почти ничего. А если внутри него запрос к базе, никакой foreach не поможет: проблема в логике, а не в синтаксисе цикла.
Мини-шпаргалка
foreach ($items as $item); // базовый обход — выбор по умолчанию
foreach ($items as $i => $item); // нужен индекс
for ($i = 0; $i < $n; $i++); // нужен именно счётчик: каждый 2-й, реверс, шаг
for (...; $i < count($arr); ...); // никогда так не делайте — count() в условии
И три вывода:
- На обходе массивов
foreachчуть быстрее или равен правильномуfor— и всегда читается проще. forберут не за скорость, а за потребность: шаг больше единицы, обратный порядок, арифметика по индексам.- Если код тормозит — профилируйте. Разница между
forиforeachпочти никогда не окажется в ответе.