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

144

Quelle est la différence de performances (le cas échéant) entre ces trois approches, toutes deux utilisées pour transformer un tableau en un autre tableau?

  1. En utilisant foreach
  2. Utilisation array_mapavec la fonction lambda / fermeture
  3. Utilisation array_mapavec une fonction / méthode 'statique'
  4. Existe-t-il une autre approche?

Pour être clair, regardons les exemples, tous faisant de même - en multipliant le tableau de nombres par 10:

$numbers = range(0, 1000);

Pour chaque

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

Carte avec lambda

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

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

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

Existe-t-il une autre approche? Je serai heureux d'entendre en fait toutes les différences entre les cas ci-dessus, et toutes les entrées pour lesquelles l'un devrait être utilisé au lieu d'autres.

Pavel S.
la source
10
Pourquoi ne pas simplement comparer et voir ce qui se passe?
Jon
17
Eh bien, je peux faire une référence. Mais je ne sais toujours pas comment cela fonctionne en interne. Même si je découvre qu'un est plus rapide, je ne sais toujours pas pourquoi. Est-ce à cause de la version PHP? Cela dépend-il des données? Y a-t-il une différence entre les tableaux associatifs et ordinaires? Bien sûr, je peux faire toute une série de points de repère, mais obtenir une théorie me fait gagner beaucoup de temps. J'espère que vous comprenez ...
Pavel S.
2
Commentaire tardif, mais n'est-ce pas while (list ($ k, $ v) = each ($ array)) plus rapide que tout ce qui précède? Je n'ai pas évalué cela dans php5.6, mais c'était dans les versions antérieures.
Owen Beresford

Réponses:

121

FWIW, je viens de faire le benchmark puisque l'affiche ne l'a pas fait. Fonctionnant sous PHP 5.3.10 + XDebug.

UPDATE 2015-01-22 comparer avec la réponse de mcfedr ci-dessous pour des résultats supplémentaires sans XDebug et une version PHP plus récente.


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 des nombres de 1M sur une douzaine de tentatives:

  • À chaque fois: 0,7 s
  • Carte à la fermeture: 3,4 s
  • Carte sur le nom de la fonction: 1,2 sec.

En supposant que la vitesse terne de la carte à la fermeture était causée par la fermeture éventuellement évaluée à chaque fois, 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 seule fois.

MISE À JOUR 02/02/2014: vidage des opcodes

Voici les vidages de l'opcode pour les trois rappels. Premièrement 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 le 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 qu'il 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 useMapNamed()fonction:


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

MGF
la source
Merci pour les benchmarks. Cependant, j'aimerais savoir pourquoi il y a une telle différence. Est-ce à cause d'une surcharge d'appel de fonction?
Pavel S.
4
J'ai ajouté les vidages de l'opcode dans le problème. La première chose que nous pouvons voir est que la fonction nommée et la fermeture ont exactement le même vidage, et elles sont appelées via array_map de la même manière, à une seule exception près: l'appel de fermeture inclut un autre opcode DECLARE_LAMBDA_FUNCTION, ce qui explique pourquoi son utilisation est un peu plus lent que d'utiliser la fonction nommée. Maintenant, en comparant la boucle de tableau aux appels de array_map, tout dans la boucle de tableau est interprété en ligne, sans aucun appel à une fonction, ce qui signifie pas de contexte à pousser / pop, juste un JMP à la fin de la boucle, ce qui explique probablement la grande différence .
FGM
4
Je viens d'essayer cela en utilisant une fonction intégrée (strtolower), et dans ce cas, useMapNamedc'est en fait plus rapide que useArray. J'ai pensé que cela valait la peine d'être mentionné.
DisgruntledGoat
1
Dans lap, ne voulez-vous pas l' range()appel au-dessus du premier appel microtime? (Bien que probablement insignifiant par rapport au temps de la boucle.)
contrebis
1
@billynoah PHP7.x est en effet beaucoup plus rapide. Il serait intéressant de voir les opcodes générés par cette version, en particulier en comparant avec / sans opcache car il fait beaucoup d'optimisations en plus de la mise en cache du code.
FGM le
232

Il est intéressant d'exécuter ce benchmark avec xdebug désactivé, car xdebug ajoute beaucoup de surcharge, en particulier pour les appels de fonction.

Ceci est le script de FGM exécuté en utilisant 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 de fermeture.

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

Ici, nous pouvons voir 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 en utilisant PHP 7 et HHVM à des fins de comparaison. Les conclusions sont similaires, même si tout est 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
mcfedr
la source
2
Je vous déclare vainqueur en brisant l'égalité et en vous donnant le 51e vote positif. TRÈS important pour s'assurer que le test n'altère pas les résultats! Question, cependant, vos temps de résultat pour "Array" sont la méthode de boucle foreach, non?
Buttle Butkus
2
Excellente réponse. C'est bien de voir à quel point 7 est rapide. Je dois commencer à l'utiliser sur mon temps personnel, toujours à 5,6 au travail.
Dan
1
Alors pourquoi devons-nous utiliser array_map au lieu de foreach? Pourquoi est-il ajouté à PHP si ses performances sont mauvaises? Y a-t-il une condition spécifique qui nécessite array_map au lieu de foreach? Existe-t-il une logique spécifique que foreach ne peut pas gérer et que array_map peut gérer?
HendraWD
3
array_map(et ses fonctions associées array_reduce, array_filter) vous permettent d'écrire un beau code. Si array_mapc'était beaucoup plus lent, ce serait une raison d'utiliser foreach, mais c'est très similaire, donc je vais l'utiliser array_mappartout où cela a du sens.
mcfedr
3
C'est bien de voir que PHP7 est considérablement amélioré. J'étais sur le point de passer à un autre langage backend pour mes projets mais je vais m'en tenir à PHP.
realnsleo
8

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 l'environnement de production LAMP sans xdebug. Je suis en train d'errer xdebug ralentirait les performances de array_map.

Clarence
la source
Je ne sais pas si vous avez eu du mal à lire la réponse de @mcfedr, mais il explique clairement que XDebug ralentit effectivement array_map;)
igorsantos07
J'ai testé les performances array_mapet l' foreachutilisation de Xhprof. Et son intéressant array_mapconsomme plus de mémoire que «foreach».
Gopal Joshi