Wydajność foreach, array_map z lambda i array_map z funkcją statyczną

144

Jaka jest różnica w wydajności (jeśli istnieje) między tymi trzema podejściami, które są używane do przekształcania tablicy w inną tablicę?

  1. Za pomocą foreach
  2. Używanie array_mapz funkcją lambda / closure
  3. Używanie array_mapz funkcją / metodą „statyczną”
  4. Czy jest jakieś inne podejście?

Aby się wyjaśnić, spójrzmy na przykłady, wszystkie robią to samo - mnożąc tablicę liczb przez 10:

$numbers = range(0, 1000);

Dla każdego

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

Mapa z lambdą

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

Mapa z funkcją „static”, przekazywana jako odniesienie do ciągu

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

Czy jest jakieś inne podejście? Z przyjemnością usłyszę właściwie wszystkie różnice między powyższymi przypadkami i wszelkie uwagi, dlaczego warto zastosować jeden zamiast innych.

Paweł S.
źródło
10
Dlaczego nie wykonasz testu porównawczego i nie zobaczysz, co się stanie?
Jon
17
Cóż, mogę zrobić punkt odniesienia. Ale nadal nie wiem, jak to działa wewnętrznie. Nawet jeśli okaże się, że jeden jest szybszy, nadal nie wiem dlaczego. Czy to z powodu wersji PHP? Czy to zależy od danych? Czy istnieje różnica między tablicami asocjacyjnymi i zwykłymi? Oczywiście mogę wykonać cały zestaw testów porównawczych, ale zdobycie pewnej teorii oszczędza tutaj dużo czasu. Mam nadzieję, że rozumiesz ...
Pavel S.
2
Spóźniony komentarz, ale czy (list ($ k, $ v) = each ($ array)) nie jest szybszy niż wszystkie powyższe? Nie testowałem tego w php5.6, ale było to we wcześniejszych wersjach.
Owen Beresford

Odpowiedzi:

121

FWIW, właśnie wykonałem test porównawczy, ponieważ plakat tego nie zrobił. Działa na PHP 5.3.10 + XDebug.

UPDATE 2015-01-22 porównaj z odpowiedzią mcfedr poniżej, aby uzyskać dodatkowe wyniki bez XDebug i nowszej wersji 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";
}

Otrzymuję dość spójne wyniki z liczbami 1M w kilkunastu próbach:

  • Na każdy: 0,7 sek
  • Mapa po zamknięciu: 3,4 sek
  • Mapa na nazwie funkcji: 1,2 sek.

Zakładając, że słaba prędkość mapy podczas zamykania była spowodowana przez ewaluację zamknięcia za każdym razem, testowałem również w ten sposób:


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

  return array_map($closure, $numbers);
}

Ale wyniki są identyczne, co potwierdza, że ​​zamknięcie jest oceniane tylko raz.

UPDATE 02-02 2014: zrzut opcodes

Oto zrzuty kodu operacji dla trzech wywołań zwrotnych. Po pierwsze 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

A później 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

i zakończenie, które nazywa:


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

wtedy useMapNamed()funkcja:


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

i nazwana funkcja, którą wywołuje _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
źródło
Dzięki za testy porównawcze. Chciałbym jednak wiedzieć, dlaczego istnieje taka różnica. Czy to z powodu narzutu wywołania funkcji?
Pavel S.
4
Dodałem zrzuty opcode w numerze. Pierwszą rzeczą, jaką widzimy, jest to, że nazwana funkcja i zamknięcie mają dokładnie ten sam zrzut i są wywoływane przez tablicę_map w podobny sposób, z jednym wyjątkiem: wywołanie zamknięcia zawiera jeszcze jeden kod operacji DECLARE_LAMBDA_FUNCTION, co wyjaśnia, dlaczego jest używane trochę wolniej niż użycie nazwanej funkcji. Teraz, porównując pętlę tablicy z wywołaniami array_map, wszystko w pętli tablicy jest interpretowane inline, bez żadnego wywołania funkcji, co oznacza brak kontekstu do wypychania / popu, tylko JMP na końcu pętli, co prawdopodobnie wyjaśnia dużą różnicę .
FGM
4
Właśnie wypróbowałem to za pomocą wbudowanej funkcji (strtolower) iw tym przypadku useMapNamedjest to faktycznie szybsze niż useArray. Pomyślałem, że warto o tym wspomnieć.
DisgruntledGoat
1
W lap, czy nie chcesz, aby range()wywołanie było wyższe niż pierwsze połączenie w mikroczasie? (Choć prawdopodobnie nieistotne w porównaniu z czasem na pętlę.)
contrebis
1
@billynoah PHP7.x jest rzeczywiście o wiele szybszy. Byłoby interesujące zobaczyć kody rozkazów generowane przez tę wersję, szczególnie w porównaniu z opcache / bez opcache, ponieważ wykonuje wiele optymalizacji oprócz buforowania kodu.
FGM
231

Interesujące jest uruchamianie tego testu przy wyłączonym xdebug, ponieważ xdebug dodaje sporo narzutów, szczególnie do wywołań funkcji.

To jest skrypt FGM uruchamiany przy użyciu 5.6 z xdebug

ForEach   : 0.79232501983643
MapClosure: 4.1082420349121
MapNamed  : 1.7884571552277

Bez xdebug

ForEach   : 0.69830799102783
MapClosure: 0.78584599494934
MapNamed  : 0.85125398635864

Tutaj jest tylko bardzo niewielka różnica między wersją przednią i zamkniętą.

Ciekawe jest również dodanie wersji z zamknięciem za pomocą use

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

Dla porównania dodaję:

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

Tutaj widzimy, że ma to wpływ na wersję zamknięcia, podczas gdy tablica nie zmieniła się zauważalnie.

19/11/2015 Dodałem teraz również wyniki z wykorzystaniem PHP 7 i HHVM do porównania. Wnioski są podobne, choć wszystko przebiega znacznie szybciej.

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
źródło
2
Ogłaszam cię zwycięzcą, rozstrzygając remis i dając ci 51 głos za. BARDZO ważne, aby upewnić się, że test nie zmieni wyników! Pytanie jednak, czy wyniki dla "Array" są metodą pętli foreach, prawda?
Buttle Butkus
2
Doskonała odpowiedź. Miło widzieć, jak szybkie jest 7. Muszę zacząć go używać w swoim prywatnym czasie, wciąż na 5,6 w pracy.
Dan
1
Dlaczego więc musimy używać array_map zamiast foreach? Dlaczego dodano go do PHP, jeśli ma słabą wydajność? Czy istnieje jakiś konkretny warunek, który wymaga tablicy_mapa zamiast foreach? Czy jest jakaś konkretna logika, której foreach nie może obsłużyć i może obsłużyć array_map?
HendraWD
3
array_map(i powiązane z nią funkcje array_reduce, array_filter) pozwalają pisać piękną kod. Gdyby array_mapbył znacznie wolniejszy, byłby to powód do użycia foreach, ale jest bardzo podobny, więc będę używać array_mapwszędzie, gdzie ma to sens.
mcfedr
3
Miło widzieć, że PHP7 zostało znacznie ulepszone. Miałem zamiar przejść na inny język zaplecza dla moich projektów, ale pozostanę przy PHP.
realnsleo
8

To interesujące. Ale mam odwrotny wynik z następującymi kodami, które są uproszczone z moich obecnych projektów:

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

Oto moje dane i kody testowe:

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

Wynik to:

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

Moje testy odbywały się w środowisku produkcyjnym LAMP bez xdebug. Błądzę, xdebug spowolniłby wydajność array_map.

Clarence
źródło
Nie jestem pewien, czy miałeś problem z odczytaniem odpowiedzi @mcfedr, ale wyjaśnia wyraźnie, że XDebug rzeczywiście zwalnia array_map;)
igorsantos 07
Testuję wydajność array_mapi foreachużywam Xhprof. A jego interesujące array_mappochłania więcej pamięci niż „foreach”.
Gopal Joshi