170

What's the performance difference (if there is any) between these three approaches, both used to transform an array to another array?

  1. Using foreach
  2. Using array_map with lambda/closure function
  3. Using array_map with 'static' function/method
  4. Is there any other approach?

To make myself clear, let's have look at the examples, all doing the same - multiplying the array of numbers by 10:

$numbers = range(0, 1000);

Foreach

$result = array();
foreach ($numbers as $number) {
    $result[] = $number * 10;
}
return $result;

Map with lambda

return array_map(function($number) {
    return $number * 10;
}, $numbers);

Map with 'static' function, passed as string reference

function tenTimes($number) {
    return $number * 10;
}
return array_map('tenTimes', $numbers);

Is there any other approach? I will be happy to hear actually all differences between the cases from above, and any inputs why one should be used instead of others.

Jake N
  • 10,535
  • 11
  • 66
  • 112
Pavel S.
  • 11,892
  • 18
  • 75
  • 113
  • 12
    Why don't you just benchmark and see what happens? – Jon Aug 09 '13 at 10:38
  • 23
    Well, I may make a benchmark. But I still do not know how it internally works. Even if I find out one is faster, I still do not know why. Is it because of the PHP version? Does it depend on the data? Is there a difference between associative and ordinary arrays? Of course I can make whole suite of benchmarks but getting some theory saves here a lot of time. I hope you understand... – Pavel S. Aug 09 '13 at 14:21
  • 3
    Late comment, but isn't while( list($k, $v)= each($array)) faster than all the above? I haven't benchmarked this in php5.6, but it was in earlier versions. – Owen Beresford Jul 01 '15 at 17:33

5 Answers5

272

Its interesting to run this benchmark with xdebug disabled, as xdebug adds quite a lot of overhead, esp to function calls.

This is FGM's script run using 5.6 With xdebug

ForEach   : 0.79232501983643
MapClosure: 4.1082420349121
MapNamed  : 1.7884571552277

Without xdebug

ForEach   : 0.69830799102783
MapClosure: 0.78584599494934
MapNamed  : 0.85125398635864

Here there is only a very small difference between the foreach and closure version.

Its also interesting to add a version with a closure with a use

function useMapClosureI($numbers) {
  $i = 10;
  return array_map(function($number) use ($i) {
      return $number * $i++;
  }, $numbers);
}

For comparison I add:

function useForEachI($numbers)  {
  $result = array();
  $i = 10;
  foreach ($numbers as $number) {
    $result[] = $number * $i++;
  }
  return $result;
}

Here we can see it makes an impact on the closure version, whereas the array hasn't noticeably changed.

19/11/2015 I have also now added results using PHP 7 and HHVM for comparison. The conclusions are similar, though everything is much faster.

PHP 5.6

ForEach    : 0.57499806880951
MapClosure : 0.59327731132507
MapNamed   : 0.69694859981537
MapClosureI: 0.73265469074249
ForEachI   : 0.60068697929382

PHP 7

ForEach    : 0.11297199726105
MapClosure : 0.16404168605804
MapNamed   : 0.11067249774933
MapClosureI: 0.19481580257416
ForEachI   : 0.10989861488342

HHVM

ForEach    : 0.090071058273315
MapClosure : 0.10432276725769
MapNamed   : 0.1091267824173
MapClosureI: 0.11197068691254
ForEachI   : 0.092114186286926
mcfedr
  • 7,845
  • 3
  • 31
  • 27
  • 2
    I declare you the winner by breaking the tie and giving you the 51st upvote. VERY important to make sure the test doesn't alter the results! Question, though, your result times for "Array" are the foreach loop method, right? – Buttle Butkus Jul 02 '16 at 03:51
  • 2
    Excellent respone. Nice to see how fast 7 is. Gotta start using it on my personal time, still at 5.6 at work. – Dan Oct 19 '16 at 22:48
  • 2
    So why we must use array_map instead of foreach? Why it added to PHP if it is bad in performance? Is there any specific condition that needs array_map instead of foreach? Is there any specific logic that foreach can't handle and array_map can handle? – HendraWD Apr 18 '17 at 13:13
  • 7
    `array_map` (and its related functions `array_reduce`, `array_filter`) let you write beautiful code. If `array_map` was much slower it would be a reason to use `foreach`, but its very similar, so I will use `array_map` everywhere it makes sense. – mcfedr Apr 20 '17 at 08:01
  • 4
    Nice to see PHP7 is vastly improved. Was about to switch to a different backend language for my projects but I will stick to PHP. – realnsleo Nov 11 '17 at 08:56
  • 1
    @HendraWD Functional code using `array_map` is much easier to read and write, and developer time is far more expensive than processor cycles. Reading a loop isn't hard, but it does take longer to realize that it's just doing a map. Better to say that up front by calling the map function. – David Harkness Feb 22 '18 at 22:02
135

FWIW, I just did the benchmark since poster didn't do it. Running on PHP 5.3.10 + XDebug.

UPDATE 2015-01-22 compare with mcfedr's answer below for additional results without XDebug and a more recent PHP version.


function lap($func) {
  $t0 = microtime(1);
  $numbers = range(0, 1000000);
  $ret = $func($numbers);
  $t1 = microtime(1);
  return array($t1 - $t0, $ret);
}

function useForeach($numbers)  {
  $result = array();
  foreach ($numbers as $number) {
      $result[] = $number * 10;
  }
  return $result;
}

function useMapClosure($numbers) {
  return array_map(function($number) {
      return $number * 10;
  }, $numbers);
}

function _tenTimes($number) {
    return $number * 10;
}

function useMapNamed($numbers) {
  return array_map('_tenTimes', $numbers);
}

foreach (array('Foreach', 'MapClosure', 'MapNamed') as $callback) {
  list($delay,) = lap("use$callback");
  echo "$callback: $delay\n";
}

I get pretty consistent results with 1M numbers across a dozen attempts:

  • Foreach: 0.7 sec
  • Map on closure: 3.4 sec
  • Map on function name: 1.2 sec.

Supposing the lackluster speed of the map on closure was caused by the closure possibly being evaluated each time, I also tested like this:


function useMapClosure($numbers) {
  $closure = function($number) {
    return $number * 10;
  };

  return array_map($closure, $numbers);
}

But the results are identical, confirming that the closure is only evaluated once.

2014-02-02 UPDATE: opcodes dump

Here are the opcode dumps for the three callbacks. First useForeach():



compiled vars:  !0 = $numbers, !1 = $result, !2 = $number
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
  10     0  >   EXT_NOP                                                  
         1      RECV                                                     1
  11     2      EXT_STMT                                                 
         3      INIT_ARRAY                                       ~0      
         4      ASSIGN                                                   !1, ~0
  12     5      EXT_STMT                                                 
         6    > FE_RESET                                         $2      !0, ->15
         7  > > FE_FETCH                                         $3      $2, ->15
         8  >   OP_DATA                                                  
         9      ASSIGN                                                   !2, $3
  13    10      EXT_STMT                                                 
        11      MUL                                              ~6      !2, 10
        12      ASSIGN_DIM                                               !1
        13      OP_DATA                                                  ~6, $7
  14    14    > JMP                                                      ->7
        15  >   SWITCH_FREE                                              $2
  15    16      EXT_STMT                                                 
        17    > RETURN                                                   !1
  16    18*     EXT_STMT                                                 
        19*   > RETURN                                                   null

Then the useMapClosure()


compiled vars:  !0 = $numbers
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
  18     0  >   EXT_NOP                                                  
         1      RECV                                                     1
  19     2      EXT_STMT                                                 
         3      EXT_FCALL_BEGIN                                          
         4      DECLARE_LAMBDA_FUNCTION                                  '%00%7Bclosure%7D%2Ftmp%2Flap.php0x7f7fc1424173'
  21     5      SEND_VAL                                                 ~0
         6      SEND_VAR                                                 !0
         7      DO_FCALL                                      2  $1      'array_map'
         8      EXT_FCALL_END                                            
         9    > RETURN                                                   $1
  22    10*     EXT_STMT                                                 
        11*   > RETURN                                                   null

and the closure it calls:


compiled vars:  !0 = $number
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
  19     0  >   EXT_NOP                                                  
         1      RECV                                                     1
  20     2      EXT_STMT                                                 
         3      MUL                                              ~0      !0, 10
         4    > RETURN                                                   ~0
  21     5*     EXT_STMT                                                 
         6*   > RETURN                                                   null

then the useMapNamed() function:


compiled vars:  !0 = $numbers
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
  28     0  >   EXT_NOP                                                  
         1      RECV                                                     1
  29     2      EXT_STMT                                                 
         3      EXT_FCALL_BEGIN                                          
         4      SEND_VAL                                                 '_tenTimes'
         5      SEND_VAR                                                 !0
         6      DO_FCALL                                      2  $0      'array_map'
         7      EXT_FCALL_END                                            
         8    > RETURN                                                   $0
  30     9*     EXT_STMT                                                 
        10*   > RETURN                                                   null

and the named function it calls, _tenTimes():


compiled vars:  !0 = $number
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
  24     0  >   EXT_NOP                                                  
         1      RECV                                                     1
  25     2      EXT_STMT                                                 
         3      MUL                                              ~0      !0, 10
         4    > RETURN                                                   ~0
  26     5*     EXT_STMT                                                 
         6*   > RETURN                                                   null

Michael Härtl
  • 8,428
  • 5
  • 35
  • 62
FGM
  • 2,830
  • 1
  • 31
  • 31
  • Thanks for the benchmarks. However, I would like to know why there is such difference. Is it because of a function call overhead? – Pavel S. Jan 25 '14 at 13:06
  • 6
    I added the opcode dumps in the issue. First thing we can see is that the named function and closure have exactly the same dump, and they are called via array_map in much the same way, with just one exception: the closure call includes one more opcode DECLARE_LAMBDA_FUNCTION, which explains why using it is a bit slower than using the named function. Now, comparing the array loop vs array_map calls, everything in the array loop is interpreted inline, without any call to a function, meaning no context to push/pop, just a JMP at the end of the loop, which likely explains the big difference. – FGM Feb 02 '14 at 17:35
  • 4
    I have just tried this using a built-in function (strtolower), and in that case, `useMapNamed` is actually faster than `useArray`. Thought that was worth mentioning. – DisgruntledGoat Aug 30 '14 at 16:57
  • 1
    In `lap`, don't you want the `range()` call above the first microtime call? (Though probably insignificant compared with the time for the loop.) – contrebis Jan 27 '15 at 15:53
  • @FGM not sure why you called it "useArray" as all methods work on an array. I've modified to "useForeach" as this is what it really does. – Michael Härtl Apr 02 '15 at 12:56
  • just ran this exact code with PHP 7.0.8 / Ubuntu 16.04.3 with the following results: Foreach: 0.092797994613647, MapClosure: 0.094599008560181, MapNamed: 0.093489170074463. – But those new buttons though.. Jan 13 '17 at 21:55
  • 1
    @billynoah PHP7.x is so much faster indeed. It would be interesting to see the opcodes generated by this version, especially comparing with/without opcache since it does a lot of optimizations besides code caching. – FGM Jan 14 '17 at 16:04
  • @FGM - I have no idea how to generate opcodes but I can if you want to explain. I was mostly surprised that although `foreach` is still fastest, the three methods have closed the gap considerably to the point of being almost identical in speed. – But those new buttons though.. Jan 14 '17 at 20:18
  • @billynoah you can use https://3v4l.org for that: eval the code, then click on the VLD tab. – FGM Feb 03 '17 at 13:08
  • If you reduce the range down to 10000, you can run a lot of tests to get a more averaged speed. Also, I noticed that passing the argument by reference seems to shave off a few moments as well. Then there's always the for count loop too. – Schenn Mar 08 '17 at 19:37
30

Here are some updated tests for the current PHP 8 (RC2) version. Also added short closures

PHP 8.0 RC2

Foreach:         0.093745978673299
MapClosure:      0.096948345502218
MapShortClosure: 0.096264243125916
MapNamed:        0.091399153073629
MapClosureI:     0.11352666219076
ForEachI:        0.097501540184021
elzorro
  • 416
  • 4
  • 6
8

It's interesting. But I've got an opposite result with the following codes which are simplified from my current projects:

// test a simple array_map in the real world.
function test_array_map($data){
    return array_map(function($row){
        return array(
            'productId' => $row['id'] + 1,
            'productName' => $row['name'],
            'desc' => $row['remark']
        );
    }, $data);
}

// Another with local variable $i
function test_array_map_use_local($data){
    $i = 0;
    return array_map(function($row) use ($i) {
        $i++;
        return array(
            'productId' => $row['id'] + $i,
            'productName' => $row['name'],
            'desc' => $row['remark']
        );
    }, $data);
}

// test a simple foreach in the real world
function test_foreach($data){
    $result = array();
    foreach ($data as $row) {
        $tmp = array();
        $tmp['productId'] = $row['id'] + 1;
        $tmp['productName'] = $row['name'];
        $tmp['desc'] = $row['remark'];
        $result[] = $tmp;
    }
    return $result;
}

// Another with local variable $i
function test_foreach_use_local($data){
    $result = array();
    $i = 0;
    foreach ($data as $row) {
        $i++;
        $tmp = array();
        $tmp['productId'] = $row['id'] + $i;
        $tmp['productName'] = $row['name'];
        $tmp['desc'] = $row['remark'];
        $result[] = $tmp;
    }
    return $result;
}

Here is my testing data and codes:

$data = array_fill(0, 10000, array(
    'id' => 1,
    'name' => 'test',
    'remark' => 'ok'
));

$tests = array(
    'array_map' => array(),
    'foreach' => array(),
    'array_map_use_local' => array(),
    'foreach_use_local' => array(),
);

for ($i = 0; $i < 100; $i++){
    foreach ($tests as $testName => &$records) {
        $start = microtime(true);
        call_user_func("test_$testName", $data);
        $delta = microtime(true) - $start;
        $records[] = $delta;
    }
}

// output result:
foreach ($tests as $name => &$records) {
    printf('%.4f : %s '.PHP_EOL, 
              array_sum($records) / count($records), $name);
}

The result is:

0.0098 : array_map
0.0114 : foreach
0.0114 : array_map_use_local
0.0115 : foreach_use_local

My tests were in LAMP production environment without xdebug. I'am wandering xdebug would slow down array_map's performance.

user221931
  • 1,852
  • 1
  • 13
  • 16
Clarence
  • 721
  • 1
  • 5
  • 9
  • Not sure if you had the trouble to read @mcfedr answer, but he explains clearly that XDebug indeed slows down `array_map` ;) – igorsantos07 Nov 03 '15 at 03:21
  • I have testing performance of `array_map` and `foreach` using Xhprof. And Its interesting `array_map` consumes more memory than ` foreach`. – Gopal Joshi Nov 14 '16 at 12:08
  • In `foreach` loops you have 3 more assignments and temporary variable creation. It's no surprise to me that's slower than one array assignment. – Jsowa Nov 04 '22 at 14:59
0

I tried testing @FGM's code on PHP 8 and window 10 in 10 times. And this is result: Image

I don't know if PHP could have JIT. I guess it had JIT in PHP8 because in file php.ini, I saw 1 config command in php.ini: auto_globals_jit=On.

Kha Tran
  • 1
  • 1
  • 1
    While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/late-answers/31603520) – Mattygabe Apr 27 '22 at 15:59