Rendimiento de foreach, array_map con lambda y array_map con función estática

144

¿Cuál es la diferencia de rendimiento (si hay alguna) entre estos tres enfoques, ambos utilizados para transformar una matriz en otra matriz?

  1. Utilizando foreach
  2. Utilizando array_mapcon la función lambda / cierre
  3. Usar array_mapcon la función / método 'estático'
  4. ¿Hay algún otro enfoque?

Para aclararme, echemos un vistazo a los ejemplos, todos haciendo lo mismo: multiplicando la matriz de números por 10:

$numbers = range(0, 1000);

Para cada

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

Mapa con lambda

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

Mapa con función 'estática', pasado como referencia de cadena

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

¿Hay algún otro enfoque? Estaré encantado de escuchar en realidad todas las diferencias entre los casos de arriba, y cualquier entrada por la cual se debe usar uno en lugar de otros.

Pavel S.
fuente
10
¿Por qué no solo comparas y ves lo que sucede?
Jon
17
Bueno, puedo hacer un punto de referencia. Pero todavía no sé cómo funciona internamente. Incluso si descubro que uno es más rápido, todavía no sé por qué. ¿Es por la versión PHP? ¿Depende de los datos? ¿Hay alguna diferencia entre las matrices asociativas y ordinarias? Por supuesto, puedo hacer un conjunto completo de puntos de referencia, pero obtener algo de teoría ahorra mucho tiempo aquí. Espero que entiendas ...
Pavel S.
2
Comentario tardío, pero ¿no es while (list ($ k, $ v) = each ($ array)) más rápido que todo lo anterior? No he comparado esto en php5.6, pero fue en versiones anteriores.
Owen Beresford

Respuestas:

121

FWIW, acabo de hacer el punto de referencia ya que el póster no lo hizo. Se ejecuta en PHP 5.3.10 + XDebug.

ACTUALIZACIÓN 2015-01-22 compare con la respuesta de mcfedr a continuación para obtener resultados adicionales sin XDebug y una versión PHP más reciente.


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

Obtengo resultados bastante consistentes con números 1M en una docena de intentos:

  • Foreach: 0.7 segundos
  • Mapa al cierre: 3,4 segundos
  • Mapa del nombre de la función: 1.2 seg.

Suponiendo que la velocidad mediocre del mapa en el cierre fue causada por el cierre posiblemente evaluado cada vez, también probé así:


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

  return array_map($closure, $numbers);
}

Pero los resultados son idénticos, lo que confirma que el cierre solo se evalúa una vez.

02/02/2014 ACTUALIZACIÓN: volcado de códigos de operación

Aquí están los volcados de código de operación para las tres devoluciones de llamada. Primero 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

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

y el cierre que llama:


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

entonces la useMapNamed()función:


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

y la función nombrada que llama _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
fuente
Gracias por los puntos de referencia. Sin embargo, me gustaría saber por qué hay tanta diferencia. ¿Se debe a una sobrecarga de llamadas a funciones?
Pavel S.
44
Agregué los volcados de opcode en el problema. Lo primero que podemos ver es que la función y el cierre nombrados tienen exactamente el mismo volcado, y se llaman a través de array_map de la misma manera, con solo una excepción: la llamada de cierre incluye un código de operación más DECLARE_LAMBDA_FUNCTION, lo que explica por qué usarlo es un poco más lento que usar la función nombrada. Ahora, al comparar el bucle de matriz con las llamadas de matriz_mapa, todo en el bucle de matriz se interpreta en línea, sin ninguna llamada a una función, lo que significa que no hay contexto para empujar / pop, solo un JMP al final del bucle, lo que probablemente explica la gran diferencia .
MGF
44
Acabo de probar esto usando una función incorporada (strtolower), y en ese caso, en useMapNamedrealidad es más rápido que useArray. Pensé que valía la pena mencionarlo.
DisgruntledGoat
1
En lap, ¿no quieres la range()llamada sobre la primera llamada de microtiempo? (Aunque probablemente sea insignificante en comparación con el tiempo para el ciclo.)
contrebis
1
@billynoah PHP7.x es mucho más rápido. Sería interesante ver los códigos de operación generados por esta versión, especialmente en comparación con / sin opcaché, ya que hace muchas optimizaciones además del almacenamiento en caché de código.
MGF
232

Es interesante ejecutar este punto de referencia con xdebug deshabilitado, ya que xdebug agrega bastante sobrecarga, especialmente a las llamadas de función.

Este es el script de FGM ejecutado usando 5.6 con xdebug

ForEach   : 0.79232501983643
MapClosure: 4.1082420349121
MapNamed  : 1.7884571552277

Sin xdebug

ForEach   : 0.69830799102783
MapClosure: 0.78584599494934
MapNamed  : 0.85125398635864

Aquí solo hay una pequeña diferencia entre la versión foreach y la versión de cierre.

También es interesante agregar una versión con un cierre con un use

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

A modo de comparación agrego:

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

Aquí podemos ver que tiene un impacto en la versión de cierre, mientras que la matriz no ha cambiado notablemente.

19/11/2015 También he agregado resultados usando PHP 7 y HHVM para comparar. Las conclusiones son similares, aunque todo es mucho más rápido.

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
fuente
2
Te declaro el ganador al romper el empate y darte el 51o voto a favor. ¡MUY importante para asegurarse de que la prueba no altere los resultados! Pregunta, sin embargo, sus tiempos de resultado para "Array" son el método de bucle foreach, ¿verdad?
Buttle Butkus
2
Excelente respuesta. Es bueno ver qué tan rápido es el 7. Tengo que empezar a usarlo en mi tiempo personal, todavía a 5.6 en el trabajo.
Dan
1
Entonces, ¿por qué debemos usar array_map en lugar de foreach? ¿Por qué se agregó a PHP si tiene un mal rendimiento? ¿Hay alguna condición específica que necesite array_map en lugar de foreach? ¿Existe alguna lógica específica que foreach no pueda manejar y que array_map pueda manejar?
HendraWD
3
array_map(y sus funciones relacionadas array_reduce, array_filter) le permiten escribir código hermoso. Si array_mapfuera mucho más lento, sería una razón para usarlo foreach, pero es muy similar, por lo que array_mapusaré en todas partes, tiene sentido.
mcfedr
3
Es bueno ver que PHP7 ha mejorado enormemente. Estaba a punto de cambiar a un lenguaje back-end diferente para mis proyectos, pero me quedaré con PHP.
realnsleo
8

Es interesante. Pero obtuve un resultado opuesto con los siguientes códigos que se simplifican de mis proyectos actuales:

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

Aquí están mis datos y códigos de prueba:

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

El resultado es:

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

Mis pruebas fueron en el entorno de producción de LAMP sin xdebug. Estoy deambulando xdebug ralentizaría el rendimiento de array_map.

Clarence
fuente
No estoy seguro si tuvo problemas para leer la respuesta de @mcfedr, pero él explica claramente que XDebug de hecho se ralentiza array_map;)
igorsantos07
Tengo pruebas de rendimiento de array_map y foreachuso de Xhprof. Y es interesante que array_mapconsume más memoria que `foreach`.
Gopal Joshi