New in PHP 8.6: Narrowing the array_map vs foreach performance gap

Tech

With PHP 8.6, array_map gets about 10% faster in a synthetic benchmark — with zero changes to your code. My colleague Tim contributed the engine-level optimization that makes this possible.

With PHP 8.6, the engine will automatically improve the following code:

function idenitityOrEnumValue(mixed $value) {
    return $value instanceof BackedEnum ? $value->value : $value;
}

$identifier = [1, 'foo', Months::JANUARY];
$identifier = \array_map(identityOrEnumValue(...), $identifier);

To turn array_map into a foreach loop as an engine optimization, PHP sees and executes this code as if it was written like this:

function idenitityOrEnumValue(mixed $value) {
    return $value instanceof BackedEnum ? $value->value : $value;
}

$identifier = [1, 'foo', Months::JANUARY];
$__tmp = [];
foreach ($identifier as $key => $value) {
    $__tmp[$key] = identityOrEnumValue($value);
}
$identifier = $__tmp;

This improves performance in a synthetic benchmark by about 10%.

For now, this only works when using first-class callables, not closures. Partial function application will also be supported right away once it becomes available.

This is the first step and provides a blueprint for further improving the performance of array_map (and related functions, array_filter, array_reduce, …) in the future.

Using array_map will always be slower than foreach, because, by design, it calls a function for each loop iteration, and each function call comes with a little overhead compared to an inline loop.

Doctrine HotPath Example

One motivating factor for working on this is a personal story from the Doctrine ORM team that I shared with Tim. A few years ago I found the following method in the “hot path” of Doctrine ORM’s loading/storing logic:

$hash = implode(' ', array_map(
    static function ($value) {
        if ($value instanceof BackedEnum) {
            return $value->value;
        }

        return $value;
    },
    $identifier,
));

When I started working on Doctrine ORM around 2010, PHP was not nearly as fast as it is today, and we developed a philosophy of optimizing the “hot path” within the Doctrine core team of contributors. Both the read and write paths of turning database rows into PHP objects and back needed to be as fast as possible.

A much more efficient implementation would look like this:

foreach ($identifier as $key => $value) {
    if ($value instanceof BackedEnum) {
       $identifier[$key] = $value->value;
    }
}

$hash = implode(' ', $identifier);

My PR back in 2024 showed a 56% performance improvement for this change, and with a hyperfine benchmark to reproduce this, I can see the foreach version running 62% faster.

hyperfine 'php -dopcache.enable_cli=1 foreach.php' 'php -dopcache.enable_cli=1 array_map.php'
Benchmark 1: php -dopcache.enable_cli=1 foreach.php
  Time (mean ± σ):     173.8 ms ±  29.3 ms    [User: 151.6 ms, System: 12.3 ms]
  Range (min … max):   158.5 ms … 260.6 ms    11 runs

Benchmark 2: php -dopcache.enable_cli=1 array_map.php
  Time (mean ± σ):     305.2 ms ±  12.6 ms    [User: 288.7 ms, System: 12.4 ms]
  Range (min … max):   295.6 ms … 330.1 ms    10 runs

Summary
  php -dopcache.enable_cli=1 foreach.php ran
    1.76 ± 0.30 times faster than php -dopcache.enable_cli=1 array_map.php

Outlook

So naturally, if the PHP engine could safely transform the code with array_map() calls with a function or closure into a foreach loop with the method call inlined, then this would be a huge win. Due to the scoping, this is difficult to achieve, but the “scope functions” RFC targeting PHP 8.6 solves this.

This kind of optimization would be in line with a similar optimization that was introduced in PHP 8.4 for sprintf(), also by my colleague Tim, and these kinds of specialized optimizations are an incremental way of how we can get more performance out of PHP each year.

Until the PHP engine can automatically transform all array_map() calls into foreach , performing these refactorings yourself is still a good idea for “hot path” code, meaning where array_map() is called frequently and takes up a large share of the request execution time. But keep in mind, this is a micro-optimization, so it is not worth your time and effort to completely avoid using array_map().

About the author

  • Benjamin

Benjamin
Founder & CEO

I am the founder and CEO of Tideways. I started the company over 10 years ago with the mission to move the PHP ecosystem forward, starting with performance. As managing director, I work across product, strategy, and the day-to-day of building a developer-focused SaaS business.

I’m a core contributor to the Doctrine open-source project and a founding board member of the PHP Foundation, which reflects my long-standing commitment to the PHP ecosystem. I particularly enjoy working at the intersection of application performance, developer experience, and the open-source community that makes PHP what it is. Outside of work, I enjoy board games, hiking, and coffee.