web-dev-qa-db-fra.com

Performances de foreach, array_map avec lambda et array_map avec une fonction statique

Quelle est la différence de performance (s’il en existe) entre ces trois approches, toutes deux utilisées pour transformer un tableau en un autre?

  1. Utiliser foreach
  2. En utilisant array_map avec fonction lambda/fermeture
  3. En utilisant array_map avec fonction/méthode 'statique'
  4. Y a-t-il une autre approche?

Pour que tout soit clair, regardons les exemples et agissons tous de la même manière, en multipliant le tableau de nombres par 10:

$numbers = range(0, 1000);

Foreach

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

Carte avec lambda

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

Carte avec la fonction 'statique', passée comme référence de chaîne

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

Y a-t-il une autre approche? Je serai heureux d'entendre réellement tous différences entre les cas d'en haut, et toutes les entrées pourquoi on devrait être utilisé à la place des autres.

126
Pavel S.

FWIW, je viens de faire le point de repère depuis que l'affiche ne l'a pas fait. En cours d'exécution sur PHP 5.3.10 + XDebug.

MISE À JOUR 2015-01-22 comparer avec la réponse de mcfedr ci-dessous pour obtenir des résultats supplémentaires sans XDebug et une version plus récente 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";
}

J'obtiens des résultats assez cohérents avec 1 million de chiffres sur une douzaine de tentatives:

  • Foreach: 0.7 sec
  • Carte sur la fermeture: 3.4 sec
  • Carte sur le nom de la fonction: 1.2 sec.

En supposant que la fermeture de la carte soit évaluée à chaque fois, la rapidité médiocre de la carte à la fermeture a également été testée. J'ai également testé comme ceci:


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

  return array_map($closure, $numbers);
}

Mais les résultats sont identiques, confirmant que la fermeture n’est évaluée qu’une fois.

MISE À JOUR DU 2014-02-02: dump des opcodes

Voici les vidages d'opcode pour les trois rappels. Première 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

Puis la 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

et la fermeture appelle:


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

puis la fonction 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

et la fonction nommée qu'il appelle, _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

105
FGM

Il est intéressant de lancer cette référence avec xdebug désactivé, car xdebug ajoute beaucoup de temps système, notamment aux appels de fonction.

Ceci est le script de FGM exécuté avec 5.6 avec xdebug

ForEach   : 0.79232501983643
MapClosure: 4.1082420349121
MapNamed  : 1.7884571552277

Sans xdebug

ForEach   : 0.69830799102783
MapClosure: 0.78584599494934
MapNamed  : 0.85125398635864

Ici, il n'y a qu'une très petite différence entre la version foreach et la version fermeture.

C'est également intéressant d'ajouter une version avec une fermeture avec un use

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

Pour comparaison j'ajoute:

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

Nous pouvons voir ici que cela a un impact sur la version de fermeture, alors que le tableau n'a pas sensiblement changé.

19/11/2015 J'ai également ajouté des résultats avec PHP 7 et HHVM à des fins de comparaison. Les conclusions sont similaires, bien que tout soit beaucoup plus rapide.

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
208
mcfedr

C'est intéressant. Mais j'ai un résultat opposé avec les codes suivants, qui sont simplifiés par rapport à mes projets actuels:

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

Voici mes données et codes de test:

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

Le résultat est:

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

Mes tests étaient dans un environnement de production LAMP sans xdebug. Je suis errant xdebug ralentirait les performances de array_map.

8
Clarence