---
tags: dev, php
title: PHP array_map vs foreach
description: an attempt to understand why array_map is slower than foreach when traversing an array
---
# PHP, array_map vs foreach
About [array_map](https://www.php.net/manual/en/function.array-map.php):
```php
array_map ( callable $callback , array $array1 [, array $... ] ) : array
```
> Applies the callback to the elements of the given arrays.
> `array_map()` returns an array containing the results of applying the callback function to the corresponding index of array1 (and ... if more arrays are provided) used as arguments for the callback.
> The number of parameters that the callback function accepts should match the number of arrays passed to array_map().
*Source: <https://www.php.net/manual/en/function.array-map.php>.*
`array_map` provides a functional-like paradigm, preventing us from using [`foreach`](https://www.php.net/manual/en/control-structures.foreach.php) loops.
And now the question that led to writing this post is:
"What is the performance overhead of using `array_map` instead of a `foreach` loop to map an array?"
[TL;DR](#Wrap-up).
*PHP version used at time of writing is **7.4.2**.*
## Benchmark
*PHP code used can be found [here](#Benchmark-Code).*
Result:
| # | [array_map.php](#array_map.php) | [foreach.php](#foreach.php) | [foreach_init.php](#foreach_init.php) |
|:-:|--:|--:|--:|
| 1 | 1.582978 | 0.107797 | 0.104314 |
| 2 | 1.502175 | 0.118180 | 0.099604 |
| 3 | 1.488776 | 0.105700 | 0.101177 |
| 4 | 1.489551 | 0.121292 | 0.100275 |
| 5 | 1.537671 | 0.106138 | 0.099592 |
| 6 | 1.550987 | 0.106349 | 0.100255 |
| 7 | 1.550303 | 0.105413 | 0.100309 |
| 8 | 1.545123 | 0.106352 | 0.101183 |
| 9 | 1.438311 | 0.105927 | 0.100107 |
| 10 | 1.618930 | 0.106874 | 0.100911 |
*Times are in seconds.*
*Code executed on a MacBook Pro with a 2.4 GHz Dual-Core Intel Core i5 processor and 8 GB of memory.*

*R code used to build this graphic can be found [here](#Benchmark-Dataviz) and executed on [RStudio.cloud](https://rstudio.cloud/project/1116509) (requires an [RStudio.cloud](https://rstudio.cloud/) account).*
When we have volume (the benchmark code processed an array of 1 000 000 entries), `foreach` appears to be much faster than `array_map`.
Also, we notice that [output array pre-allocation](#foreach_init.php) doesn't impact performance in any way.
But how does `array_map` work?
The rest of the post is an attempt to dive into the implementation of `array_map`.
## array_map Implementation
`array_map` is implemented in [array.c](https://github.com/php/php-src/blob/36935e42eac054a422b7edade69923db7727f268/ext/standard/array.c#L6101).
### Array traversal
`array_map` array traversal uses core macros:
- [ZEND_HASH_FOREACH](https://github.com/php/php-src/blob/master/Zend/zend_hash.h#L917):
```c
#define ZEND_HASH_FOREACH(_ht, indirect) do { \
HashTable *__ht = (_ht); \
Bucket *_p = __ht->arData; \
Bucket *_end = _p + __ht->nNumUsed; \
for (; _p != _end; _p++) { \
zval *_z = &_p->val; \
if (indirect && Z_TYPE_P(_z) == IS_INDIRECT) { \
_z = Z_INDIRECT_P(_z); \
} \
if (UNEXPECTED(Z_TYPE_P(_z) == IS_UNDEF)) continue;
```
- [ZEND_HASH_FOREACH_END](https://github.com/php/php-src/blob/master/Zend/zend_hash.h#L941):
```c
#define ZEND_HASH_FOREACH_END() \
} \
} while (0)
```
- [ZEND_HASH_FOREACH_KEY_VAL_IND](https://github.com/php/php-src/blob/master/Zend/zend_hash.h#L1016):
```c
#define ZEND_HASH_FOREACH_KEY_VAL_IND(ht, _h, _key, _val) \
ZEND_HASH_FOREACH(ht, 1); \
_h = _p->h; \
_key = _p->key; \
_val = _z;
```
An array is traversed this way:
```c
ZEND_HASH_FOREACH_KEY_VAL_IND(Z_ARRVAL(arrays[0]), num_key, str_key, zv) {
// ...
} ZEND_HASH_FOREACH_END();
```
If we expand the core macros, this gives us:
```c
zend_ulong num_key;
zend_string *str_key;
zval *zv;
do {
HashTable *__ht = (Z_ARRVAL(arrays[0]));
Bucket *_p = __ht->arData;
Bucket *_end = _p + __ht->nNumUsed;
for (; _p != _end; _p++) {
zval *_z = &_p->val;
if (indirect && Z_TYPE_P(_z) == IS_INDIRECT) {
_z = Z_INDIRECT_P(_z);
}
if (UNEXPECTED(Z_TYPE_P(_z) == IS_UNDEF)) continue;
num_key = _p->h;
str_key = _p->key;
zv = _z;
{
// ...
}
}
} while (0)
```
### Callback
Let's focus on the callback invocation:
```c
zval *zv, arg;
int ret;
zend_fcall_info fci = empty_fcall_info;
zend_fcall_info_cache fci_cache = empty_fcall_info_cache;
{
fci.retval = &result;
fci.param_count = 1;
fci.params = &arg;
fci.no_separation = 0;
ZVAL_COPY(&arg, zv);
ret = zend_call_function(&fci, &fci_cache);
// Some check-code was removed for clarity.
if (str_key) {
_zend_hash_append(Z_ARRVAL_P(return_value), str_key, &result);
} else {
zend_hash_index_add_new(Z_ARRVAL_P(return_value), num_key, &result);
}
}
```
1. `zv` is a pointer to the array current value.
`arg` is a pointer to the value to pass to the callback.
*They both have type [`zval`](https://github.com/php/php-src/blob/43443857b74503246ee4ca25859b302ed0ebc078/Zend/zend_types.h#L302).*
2. `zv` is copied to `arg` using [ZVAL_COPY](https://github.com/php/php-src/blob/43443857b74503246ee4ca25859b302ed0ebc078/Zend/zend_types.h#L1213) (which internaly calls [ZVAL_COPY_VALUE_EX](https://github.com/php/php-src/blob/43443857b74503246ee4ca25859b302ed0ebc078/Zend/zend_types.h#L1187)):
*`w2`, of type `uint32_t`, is a member of [zend_value](https://github.com/php/php-src/blob/43443857b74503246ee4ca25859b302ed0ebc078/Zend/zend_types.h#L282).*
```c
arg->value.ww.w2 = zv->value.ww.w2;
```
3. Callback is invoked using [zend_call_function](https://github.com/php/php-src/blob/master/Zend/zend_execute_API.c#L642) function.
*Lot of things happen in this function before executing the callback.*
4. Output array is filled using [_zend_hash_append](https://github.com/php/php-src/blob/33ef3d64dac366733f2af40d5bce2bac4e5bca1e/Zend/zend_hash.h#L1143) (for string key) or [zend_hash_index_add_new](https://github.com/php/php-src/blob/dcbf020f76a2568b855508c812f68170884398da/Zend/zend_hash.c#L1070) (for numeric key).
## Wrap up
While `array_map` offers a beautiful way to perform array mapping, it appears to be a **slower** solution with **huge arrays**.
This can be explained by the extra processing internaly done, which makes it safe and robust (handling of reference counter, etc.).
Even if `foreach` is a **faster** solution, extreme **precautions** will have to be taken when used, as we'll have to take care of everything on our own.
This is each developper decision to find the right trade-off between code speed and clarity/maintainability.
## Annex
### Benchmark Code
Code used for benchmarking.
#### array_create.php
```php
<?php
CONST ENTRY_COUNT = 1000000;
function array_create(int $count = ENTRY_COUNT): array {
return array_map(
function($value) {
return rand();
},
range(0, ENTRY_COUNT - 1)
);
}
```
#### array_map.php
```php
<?php
require_once "array_create.php";
$in = array_create();
$start = microtime(true);
$out = array_map(
function($value) {
return $value;
},
$in
);
$end = microtime(true);
echo sprintf("%f\n", $end - $start);
```
#### foreach.php
```php
<?php
require_once "array_create.php";
$in = array_create();
$start = microtime(true);
$out = [];
foreach ($in as $key => $value) {
$out[$key] = $value;
}
$end = microtime(true);
echo sprintf("%f\n", $end - $start);
```
*Output array is built on the fly.*
#### foreach_init.php
```php
<?php
require_once "array_create.php";
$in = array_create();
$start = microtime(true);
$out = array_fill(0, count($in), null);
foreach ($in as $key => $value) {
$out[$key] = $value;
}
$end = microtime(true);
echo sprintf("%f\n", $end - $start);
```
*Output array is pre-allocated.*
#### Benchmark Script
Shell script:
```bash
for pass in {1..10}
do
echo "Pass ${pass}"
php -f array_map.php
php -f foreach.php
php -f foreach_init.php
done
```
#### Benchmark Dataviz
[R](https://cran.r-project.org/) Code:
```r
library(tidyverse)
bench_data <- tibble(
pass = 1:10,
array_map = c(1.582978, 1.502175, 1.488776, 1.489551, 1.537671, 1.550987, 1.550303, 1.545123, 1.438311, 1.618930),
foreach = c(0.107797, 0.118180, 0.105700, 0.121292, 0.106138, 0.106349, 0.105413, 0.106352, 0.105927, 0.106874),
foreach_init = c(0.104314, 0.099604, 0.101177, 0.100275, 0.099592, 0.100255, 0.100309, 0.101183, 0.100107, 0.100911)
)
bench_data_long <- bench_data %>%
pivot_longer(cols = c(array_map, foreach, foreach_init), names_to = "method", values_to = "time") %>%
mutate(method = as.factor(method))
bench_data_long %>%
ggplot(aes(pass, time, group = method, colour = method)) +
geom_line() +
geom_point() +
scale_x_continuous("Pass", limits = range(bench_data_long$pass), breaks = bench_data$pass) +
scale_y_continuous("Execution Time (sec)", limits = c(0, max(bench_data_long$time))) +
ggtitle("PHP array_map vs foreach Benchmark")
```