Leistung von foreach, array_map mit Lambda und array_map mit statischer Funktion

144

Was ist der Leistungsunterschied (falls vorhanden) zwischen diesen drei Ansätzen, die beide zum Transformieren eines Arrays in ein anderes Array verwendet werden?

  1. Verwenden von foreach
  2. Verwendung array_mapmit Lambda / Verschlussfunktion
  3. Verwendung array_mapmit 'statischer' Funktion / Methode
  4. Gibt es einen anderen Ansatz?

Um mich klar zu machen, schauen wir uns die Beispiele an, die alle dasselbe tun - multiplizieren Sie das Zahlenfeld mit 10:

$numbers = range(0, 1000);

Für jedes

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

Karte mit Lambda

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

Map mit 'statischer' Funktion, übergeben als String-Referenz

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

Gibt es einen anderen Ansatz? Ich werde mich freuen, tatsächlich alle Unterschiede zwischen den Fällen von oben und alle Eingaben zu hören, warum einer anstelle anderer verwendet werden sollte.

Pavel S.
quelle
10
Warum messen Sie nicht einfach und sehen, was passiert?
Jon
17
Nun, ich kann einen Benchmark machen. Aber ich weiß immer noch nicht, wie es intern funktioniert. Selbst wenn ich herausfinde, dass einer schneller ist, weiß ich immer noch nicht warum. Liegt es an der PHP-Version? Kommt es auf die Daten an? Gibt es einen Unterschied zwischen assoziativen und gewöhnlichen Arrays? Natürlich kann ich eine ganze Reihe von Benchmarks erstellen, aber das Erhalten einer Theorie spart hier viel Zeit. Ich hoffe du verstehst ...
Pavel S.
2
Später Kommentar, aber ist while (Liste ($ k, $ v) = jedes ($ array)) nicht schneller als alle oben genannten? Ich habe dies in PHP5.6 nicht verglichen, aber es war in früheren Versionen.
Owen Beresford

Antworten:

121

FWIW, ich habe gerade den Benchmark gemacht, da das Poster es nicht gemacht hat. Läuft unter PHP 5.3.10 + XDebug.

UPDATE 22.01.2015 Vergleiche mit der Antwort von mcfedr unten, um zusätzliche Ergebnisse ohne XDebug und eine neuere PHP-Version zu erhalten.


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

Ich erhalte ziemlich konsistente Ergebnisse mit 1 Million Zahlen über ein Dutzend Versuche:

  • Foreach: 0,7 Sek
  • Karte bei Schließung: 3,4 Sek
  • Karte auf Funktionsname: 1,2 Sek.

Angenommen, die glanzlose Geschwindigkeit der Karte beim Schließen wurde dadurch verursacht, dass der Abschluss möglicherweise jedes Mal ausgewertet wird, habe ich auch Folgendes getestet:


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

  return array_map($closure, $numbers);
}

Die Ergebnisse sind jedoch identisch, was bestätigt, dass der Verschluss nur einmal ausgewertet wird.

2014-02-02 UPDATE: Opcodes-Dump

Hier sind die Opcode-Dumps für die drei Rückrufe. Erstens 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

Dann ist die 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

und die Schließung, die es nennt:


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

dann die useMapNamed()Funktion:


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

und die benannte Funktion, die es aufruft , _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

FGM
quelle
Danke für die Benchmarks. Ich würde jedoch gerne wissen, warum es einen solchen Unterschied gibt. Liegt es an einem Funktionsaufruf-Overhead?
Pavel S.
4
Ich habe die Opcode-Dumps in der Ausgabe hinzugefügt. Das erste, was wir sehen können, ist, dass die benannte Funktion und der Abschluss genau den gleichen Speicherauszug haben und über array_map auf die gleiche Weise aufgerufen werden, mit nur einer Ausnahme: Der Abschlussaufruf enthält einen weiteren Opcode DECLARE_LAMBDA_FUNCTION, der erklärt, warum er verwendet wird etwas langsamer als mit der genannten Funktion. Wenn Sie nun die Array-Schleife mit den Array_Map-Aufrufen vergleichen, wird alles in der Array-Schleife inline interpretiert, ohne dass eine Funktion aufgerufen wird. Dies bedeutet, dass kein Kontext zum Push / Pop erforderlich ist, sondern nur ein JMP am Ende der Schleife, was wahrscheinlich den großen Unterschied erklärt .
FGM
4
Ich habe dies gerade mit einer eingebauten Funktion (strtolower) versucht und ist in diesem Fall useMapNamedtatsächlich schneller als useArray. Ich dachte, das wäre erwähnenswert.
DisgruntledGoat
1
In lap, wollen nicht , dass Sie den range()Anruf über dem ersten micro Anruf? (Obwohl wahrscheinlich unbedeutend im Vergleich zur Zeit für die Schleife.)
contrebis
1
@billynoah PHP7.x ist in der Tat so viel schneller. Es wäre interessant, die von dieser Version generierten Opcodes zu sehen, insbesondere im Vergleich mit / ohne Opcache, da neben dem Code-Caching viele Optimierungen vorgenommen werden.
FGM
231

Es ist interessant, diesen Benchmark mit deaktiviertem xdebug auszuführen, da xdebug viel Aufwand verursacht, insbesondere bei Funktionsaufrufen.

Dies ist das Skript von FGM, das mit 5.6 With xdebug ausgeführt wird

ForEach   : 0.79232501983643
MapClosure: 4.1082420349121
MapNamed  : 1.7884571552277

Ohne xdebug

ForEach   : 0.69830799102783
MapClosure: 0.78584599494934
MapNamed  : 0.85125398635864

Hier gibt es nur einen sehr kleinen Unterschied zwischen der Foreach- und der Closure-Version.

Es ist auch interessant, eine Version mit einem Verschluss mit einem hinzuzufügen use

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

Zum Vergleich füge ich hinzu:

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

Hier können wir sehen, dass es sich auf die Abschlussversion auswirkt, während sich das Array nicht merklich geändert hat.

19/11/2015 Ich habe jetzt auch Ergebnisse mit PHP 7 und HHVM zum Vergleich hinzugefügt. Die Schlussfolgerungen sind ähnlich, obwohl alles viel schneller ist.

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
quelle
2
Ich erkläre Sie zum Gewinner, indem ich das Unentschieden breche und Ihnen die 51. Gegenstimme gebe. SEHR wichtig, um sicherzustellen, dass der Test die Ergebnisse nicht verändert! Frage, Ihre Ergebniszeiten für "Array" sind die foreach-Schleifenmethode, oder?
Buttle Butkus
2
Hervorragende Antwort. Schön zu sehen, wie schnell 7 ist. Ich muss es in meiner persönlichen Zeit verwenden, immer noch um 5.6 Uhr bei der Arbeit.
Dan
1
Warum müssen wir also array_map anstelle von foreach verwenden? Warum wurde es zu PHP hinzugefügt, wenn die Leistung schlecht ist? Gibt es eine bestimmte Bedingung, die array_map anstelle von foreach benötigt? Gibt es eine bestimmte Logik, die foreach nicht verarbeiten kann und array_map verarbeiten kann?
HendraWD
3
array_map(und die damit verbundene Funktionen array_reduce, array_filter) können Sie schönen Code schreiben. Wenn array_mapes viel langsamer wäre, wäre es ein Grund, es zu verwenden foreach, aber es ist sehr ähnlich, also werde ich es array_mapüberall verwenden, wo es Sinn macht.
McFedr
3
Schön zu sehen, dass PHP7 erheblich verbessert wurde. Ich wollte gerade für meine Projekte zu einer anderen Backend-Sprache wechseln, aber ich werde mich an PHP halten.
Realnsleo
8

Es ist interessant. Aber ich habe ein gegenteiliges Ergebnis mit den folgenden Codes, die aus meinen aktuellen Projekten vereinfacht wurden:

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

Hier sind meine Testdaten und 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);
}

Das Ergebnis ist:

0,0098: array_map
0,0114: foreach
0.0114: array_map_use_local
0.0115: foreach_use_local

Meine Tests wurden in einer LAMP-Produktionsumgebung ohne xdebug durchgeführt. Ich wandere durch xdebug, was die Leistung von array_map verlangsamen würde.

Clarence
quelle
Ich bin mir nicht sicher, ob du die Mühe hattest, die Antwort von @mcfedr zu lesen, aber er erklärt klar, dass XDebug tatsächlich langsamer wird array_map;)
igorsantos07
Ich habe Testleistung von array_mapund foreachmit Xhprof. Und sein interessantes array_mapverbraucht mehr Speicher als "foreach".
Gopal Joshi