Performance implications of different loop types
Concept
Loop performance in PHP has several non-obvious characteristics. The most impactful choices are about what happens inside and before the loop, not the loop construct itself (for vs foreach have negligible overhead differences in modern PHP).
Count caching: for ($i = 0; $i < count($arr); $i++) calls count() on every iteration. count() on a PHP array is O(1) (the count is pre-computed in the array struct), but there is still a function call overhead per iteration. Cache it: for ($i = 0, $n = count($arr); $i < $n; $i++). For foreach, this is irrelevant — PHP pre-fetches the iterator state.
Reference vs copy in foreach: foreach($arr as $item) works on a copy of the array's iteration state (not a full copy of the data due to COW). foreach($arr as &$item) passes elements by reference, which disables COW and can increase memory pressure on large arrays. For read-only iteration, the value copy is fine and sometimes faster.
Function calls in loops: Every function call in PHP has overhead (~50-100ns). Calling strlen(), count(), or custom functions on every iteration adds up in hot loops processing millions of records. Pre-compute once before the loop.
array_map vs foreach: array_map is slightly slower than foreach for simple operations because it has function-call overhead per element plus the overhead of building the result array. For functional chains, it's worth it for clarity. For raw throughput, foreach with direct manipulation is faster.
Generators beat arrays when processing large datasets — see the generators lesson for details.
Code Example
<?php
declare(strict_types=1);
// Bad: count() called every iteration (even if O(1), still overhead)
$arr = range(1, 100_000);
for ($i = 0; $i < count($arr); $i++) { /* ... */ }
// Good: cache count
for ($i = 0, $n = count($arr); $i < $n; $i++) { /* ... */ }
// Function call in loop — bad for tight loops
$strings = array_fill(0, 100_000, 'Hello World');
foreach ($strings as $s) {
if (strlen($s) > 5) { /* process */ } // strlen() per iteration
}
// Better: if length is constant, cache it
$len = 11;
foreach ($strings as $s) {
if ($len > 5) { /* process */ }
}
// array_map vs foreach benchmark pattern
$data = range(1, 100_000);
$t1 = microtime(true);
$result = array_map(fn($x) => $x * 2, $data);
echo "array_map: " . round((microtime(true)-$t1)*1000, 2) . "ms\n";
$t1 = microtime(true);
$result = [];
foreach ($data as $x) {
$result[] = $x * 2;
}
echo "foreach: " . round((microtime(true)-$t1)*1000, 2) . "ms\n";
// foreach is typically ~20% faster for simple operations
// Early termination saves iterations
function findFirst(array $arr, callable $pred): mixed
{
foreach ($arr as $item) {
if ($pred($item)) return $item; // breaks as soon as found
}
return null;
}
// Avoid in_array in loops — O(n) per call = O(n²) total
$blacklist = ['baduser1', 'baduser2', 'baduser3'];
$users = array_fill(0, 10_000, 'someuser');
// Bad: O(n²)
foreach ($users as $u) {
if (in_array($u, $blacklist)) { /* ... */ }
}
// Good: O(n) total
$blacklistHash = array_flip($blacklist);
foreach ($users as $u) {
if (isset($blacklistHash[$u])) { /* ... */ } // O(1) lookup
}