Foreach、具有 lambda 的 array_map 和具有 static 函数的 array_map 的性能

这三种方法都用于将数组转换为另一个数组,它们之间的性能差异是什么(如果有的话) ?

  1. 使用 foreach
  2. 使用具有 lambda/close 函数的 array_map
  3. 使用具有“ static”函数/方法的 array_map
  4. 还有别的办法吗?

为了让我自己清楚,让我们看看这些例子,所有的例子都是一样的——把数组乘以10:

$numbers = range(0, 1000);

前伸

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

和 Lambda 的地图

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

使用“ static”函数映射,作为字符串引用传递

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

还有别的办法吗?我将很高兴听到实际上 所有之间的差异从上面的情况,以及任何输入为什么应该使用一个而不是其他。

100785 次浏览

FWIW,我只是做了基准,因为海报没有这样做。运行在 PHP 5.3.10 + XDebug。

UPDATE 2015-01-22与 mcfedr 的回答相比,下面的结果没有 XDebug 和更新的 PHP 版本。


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";
}


我通过十几次尝试得到了相当一致的100万个数字:

  • Foreach: 0.7秒
  • 关闭地图: 3.4秒
  • 映射到函数名: 1.2秒。

假设闭包上映射的暗淡速度是由每次可能被评估的闭包引起的,我也是这样测试的:


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


return array_map($closure, $numbers);
}

但是结果是相同的,证实了闭包只被评估一次。

2014-02-02更新: 操作码转储

下面是三个回调函数的操作码转储:




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

然后是 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

以及它所谓的结束:


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

然后是 useMapNamed()函数:


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

以及它调用的命名函数 _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


在禁用 xdebug 的情况下运行这个基准测试很有趣,因为 xdebug 增加了相当多的开销,尤其是函数调用。

这是 FGM 的脚本运行使用5.6 用 xdebug

ForEach   : 0.79232501983643
MapClosure: 4.1082420349121
MapNamed  : 1.7884571552277

没有 xdebug

ForEach   : 0.69830799102783
MapClosure: 0.78584599494934
MapNamed  : 0.85125398635864

在这里,foreach 和闭包版本之间只有很小的差异。

添加一个带有 use闭包的版本也很有趣

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

为了进行比较,我补充道:

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

在这里,我们可以看到它对闭包版本产生了影响,而数组没有明显改变。

19/11/2015我现在还添加了使用 PHP 7和 HHVM 进行比较的结果。结论是相似的,虽然一切都要快得多。

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

这很有趣,但是我得到了一个相反的结果,下面的代码是从我目前的项目中简化过来的:

// 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;
}

这是我的测试数据和代码:

$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);
}

结果是:

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

我的测试是在没有 xdebug 的 LAMP 生产环境中进行的。 我在徘徊 xdebug 会降低 array _ map 的性能。

下面是当前 PHP8(RC2)版本的一些更新测试。 还增加了短闭包

PHP 8.0 RC2


Foreach:         0.093745978673299
MapClosure:      0.096948345502218
MapShortClosure: 0.096264243125916
MapNamed:        0.091399153073629
MapClosureI:     0.11352666219076
ForEachI:        0.097501540184021

我尝试了10次在 PHP 8和 Windows 10上测试@FGM 的代码,结果是: 形象

我不知道 PHP 是否可以有 JIT。我猜它在 PHP8中有 JIT,因为在 php.ini 文件中,我看到了 php.ini 中的1个配置命令: auto _ globals _ JIT = On。