The Warning That Started It All ⚠️
Recently, while working on a PHP script that merged arrays inside a loop, I saw a warning from the PHP Inspections (EA Extended) tool:
'array_merge(...)' is used in a loop and is a resource-greedy construction.
I’ve learnt to trust this tool. In the past, its tips have led me to surprising performance discoveries — like in my earlier post about static vs non-static closures in PHP. So when it flagged this, I wanted to know: is merging arrays inside a loop really that bad? Or is it just one of those “best practices” that sound good but don’t matter much?
The tool’s documentation also offered alternative approaches that might be faster. But I didn’t want to just take its word for it — I wanted proof. So I set out to run benchmarks in PHP 8.
Setting Up the Benchmark 🧪
For this test, I used PHPBench. I created a benchmark class with different methods:
-
array_merge()
inside a loop. - Spread operator (
...
) inside a loop. - Collecting data and merging once at the end.
Each scenario was tested with:
- Unique values.
- Repeated values.
- Multiple values per iteration.
The loop ran 10,000 iterations per test, and each test was repeated 50 times.
Benchmark Code:
<?php
declare(strict_types=1);
namespace Tests\App\Benchmark\MergeArrayInLoop;
use PhpBench\Attributes\Iterations;
#[Iterations(50)]
class MergingArraysInALoopBench
{
private const int ITERATIONS = 10000;
public function bench_merging_unique_in_a_loop(): void
{
$result = array_fill(0, self::ITERATIONS, 0);
for ($i = 0; $i < self::ITERATIONS; $i++) {
$result = array_merge($result, [$i]);
}
}
public function bench_merging_non_unique_in_a_loop(): void
{
$result = array_fill(0, self::ITERATIONS, 0);
for ($i = 0; $i < self::ITERATIONS; $i++) {
$result = array_merge($result, [0]);
}
}
public function bench_merging_multiple_value_arrays_in_a_loop(): void
{
$result = array_fill(0, self::ITERATIONS, 0);
for ($i = 0; $i < self::ITERATIONS; $i++) {
$result = array_merge($result, [$i, $i + 1, $i + self::ITERATIONS]);
}
}
public function bench_merging_unique_in_a_loop_using_spread_operator(): void
{
$result = array_fill(0, self::ITERATIONS, 0);
for ($i = 0; $i < self::ITERATIONS; $i++) {
$result = [...$result, ...[$i]];
}
}
public function bench_merging_non_unique_in_a_loop_using_spread_operator(): void
{
$result = array_fill(0, self::ITERATIONS, 0);
for ($i = 0; $i < self::ITERATIONS; $i++) {
$result = [...$result, ...[0]];
}
}
public function bench_merging_multiple_value_arrays_in_a_loop_using_spread_operator(): void
{
$result = array_fill(0, self::ITERATIONS, 0);
for ($i = 0; $i < self::ITERATIONS; $i++) {
$result = [...$result, ...[$i, $i + 1, $i + self::ITERATIONS]];
}
}
public function bench_collecting_unique_and_merging_at_the_end(): void
{
$result = [array_fill(0, self::ITERATIONS, 0)];
for ($i = 0; $i < self::ITERATIONS; $i++) {
$result[] = [$i];
}
$result = array_merge(...$result);
}
public function bench_collecting_non_unique_and_merging_at_the_end(): void
{
$result = [array_fill(0, self::ITERATIONS, 0)];
for ($i = 0; $i < self::ITERATIONS; $i++) {
$result[] = [0];
}
$result = array_merge(...$result);
}
public function bench_collecting_multiple_value_arrays_and_merging_at_the_end(): void
{
$result = [array_fill(0, self::ITERATIONS, 0)];
for ($i = 0; $i < self::ITERATIONS; $i++) {
$result[] = [$i, $i + 1, $i + self::ITERATIONS];
}
$result = array_merge(...$result);
}
}
Benchmark Results 📊
+---------------------------------------------------------------------+-----+-------------+-------------+--------------+-----------+-----------+
| subject | its | memory_real | memory_peak | memory_final | mode_time | mean_time |
+---------------------------------------------------------------------+-----+-------------+-------------+--------------+-----------+-----------+
| bench_merging_unique_in_a_loop | 50 | 4.19mb | 1.62mb | 0.54mb | 147.75ms | 148.19ms |
| bench_merging_non_unique_in_a_loop | 50 | 4.19mb | 1.62mb | 0.54mb | 148.62ms | 148.35ms |
| bench_merging_multiple_value_arrays_in_a_loop | 50 | 4.19mb | 2.67mb | 0.54mb | 250.22ms | 250.81ms |
| bench_merging_unique_in_a_loop_using_spread_operator | 50 | 4.19mb | 1.62mb | 0.54mb | 147.14ms | 147.53ms |
| bench_merging_non_unique_in_a_loop_using_spread_operator | 50 | 4.19mb | 1.62mb | 0.54mb | 147.60ms | 147.34ms |
| bench_merging_multiple_value_arrays_in_a_loop_using_spread_operator | 50 | 4.19mb | 2.67mb | 0.54mb | 248.54ms | 248.48ms |
| bench_collecting_unique_and_merging_at_the_end | 50 | 4.19mb | 3.78mb | 0.54mb | 1.27ms | 1.28ms |
| bench_collecting_non_unique_and_merging_at_the_end | 50 | 2.10mb | 1.62mb | 0.54mb | 0.64ms | 0.66ms |
| bench_collecting_multiple_value_arrays_and_merging_at_the_end | 50 | 4.19mb | 4.30mb | 0.54mb | 1.51ms | 1.59ms |
+---------------------------------------------------------------------+-----+-------------+-------------+--------------+-----------+-----------+
What the Results Show 🔍
- Merging inside the loop — whether with
array_merge()
or spread — is much slower (147–250 ms) than merging once at the end. - The spread operator didn’t offer any speed advantage over
array_merge()
in these cases. - Merging once at the end was 100x faster (0.64–1.59 ms), but with a trade-off: higher peak memory usage (up to 4.3 MB).
Practical Takeaways 💡
- If speed matters most – Gather data first, merge at the end.
- If memory is tight – Merge in smaller steps to keep peak memory lower.
-
Don’t expect magic from the spread operator – It’s no faster here than
array_merge()
. - Benchmark in your own environment – Hardware, PHP version, and dataset size can change results.
Wrapping It Up 🏁
Performance advice should never be taken blindly. Tools like PHP Inspections are great at pointing out potential bottlenecks, but only real measurements will tell you if an optimisation is worth it in your situation.
Top comments (1)
I wonder what made you use array_merge in a loop?