Estamos preparando PHP. ¿Cómo son while, foreach, array_walk y algunas otras palabras de miedo



Era de noche, no había nada que hacer. Es hora de organizar un pequeño análisis de cómo difieren internamente algunos métodos de clasificación de matrices en PHP.

Fuentes de la rama maestra (ahora es 7.4 intercalado con 8)
El generador de código de operación de php 7.3.0.
Las mediciones se realizaron en 7.3.6.

Descargo de responsabilidad para los nerds: mencionar un par de nanosegundos y ciclos de procesador es un truco polémico llamado "hipérbole".

Tal vez, de hecho, hay decenas o cientos de nanosegundos y miles de medidas, pero aún es tan pequeño que la necesidad de ahorrar en ellos sugiere que algo en su código está mal.

Etapa de compilación


for , foreach , do y while son palabras clave del lenguaje, mientras que las funciones array_ * son funciones de la biblioteca estándar. Por lo tanto, ceteris paribus, de acuerdo con el primero, el analizador ejecutará un par de nanosegundos más rápido.

Analizador


Hasta el token de la declaración , la ruta será la misma para todos

start -> top_statement_list -> top_statement -> statement 

Los bucles se definen a nivel de instrucción :
 ->T_FOR '(' for_exprs ';' for_exprs ';' for_exprs ')' for_statement ->T_FOREACH '(' expr T_AS foreach_variable ')' foreach_statement ->T_FOREACH '(' expr T_AS foreach_variable T_DOUBLE_ARROW foreach_variable ')' foreach_statement ->T_DO statement T_WHILE '(' expr ')' ';' ->T_WHILE '(' expr ')' while_statement 

La diferencia entre for_exprs y solo expr es solo que para el primero, se permite escribir varios expr , separados por comas.

foreach_variable es una construcción que, además de solo variable , también rastrea la descompresión usando list o [] .

for_statement , foreach_statement , while_statement difieren de la declaración estándar en que agregan la capacidad de analizar una sintaxis alternativa:

 foreach($array as $element): #do something endforeach; 

La llamada a la función está enterrada mucho más profundo:

 -> expr -> variable -> callable_variable -> function_call -> name argument_list 

callable_variable , hmm ... Gracioso, ¿no? :)

Pasemos a los códigos de operación.


Como ejemplo, tomemos una iteración simple de una matriz indexada con la impresión de cada clave y valor. Está claro que usar for , while y do para tal tarea no está justificado, pero nuestro objetivo es simplemente mostrar el dispositivo interno.

foreach


 $arr = ['a', 'b', 'c']; //        foreach($arr as $key => $value) echo $key.$value; 

 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 2 0 E > ASSIGN !0, <array> 4 1 > FE_RESET_R $4 !0, ->7 2 > > FE_FETCH_R ~5 $4, !1, ->7 3 > ASSIGN !2, ~5 4 CONCAT ~7 !2, !1 5 ECHO ~7 6 > JMP ->2 7 > FE_FREE $4 5 8 > RETURN 1 

¿Qué está pasando aquí?


FE_RESET_R : crea un iterador de $ 4 para la matriz ! 0 . O, si la matriz está vacía, inmediatamente pasa a la instrucción 7 .
FE_FETCH_R : realiza un paso de iteración, recupera la clave actual en ~ 5 y el valor en ! 1 . O, si se alcanza el final de la matriz, continúa con la instrucción 7 .
Las instrucciones 3-6 no son particularmente interesantes. Aquí hay una conclusión y regreso a FE_FETCH_R .
FE_FREE : destruye el iterador.

para, mientras, ...


 $length = count($arr); for($i = 0; $i < $length; $i++) echo $i.$arr[$i]; 

Este es realmente un caso especial de while

 $i = 0; while($i < $length) { echo $i.$arr[$i]; $i++; } 

Este es realmente un caso especial de if + goto

 $i = 0; goto check; body: echo $i.$arr[$i]; $i++; check: if($i < $length) goto body; 

Los códigos de operación para los tres casos serán casi idénticos. A menos que en el caso de if , JMPNZ cambie a un par de JMPZ + JMP debido a la entrada en el cuerpo de if .
Para el bucle do , los códigos de operación variarán ligeramente debido a su naturaleza posterior a la verificación.

 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 3 0 E > ASSIGN !0, <array> 5 1 COUNT ~4 !0 2 ASSIGN !1, ~4 6 3 ASSIGN !2, 0 4 > JMP ->10 5 > FETCH_DIM_R ~7 !0, !2 6 CONCAT ~8 !2, ~7 7 ECHO ~8 8 POST_INC ~9 !2 9 FREE ~9 10 > IS_SMALLER ~10 !2, !1 11 > JMPNZ ~10, ->5 12 > > RETURN 1 

Como se esperaba, hay más códigos de operación.
0-2 : calculando $ longitud .
3: $ i = 0
4, 10, 11 : verificación inicial de la condición de salida y, de hecho, ya sea la salida o la transición al cuerpo del ciclo.
5 (FETCH_DIM_R): $ arr [$ i]
6-7 : conclusión
8-9: $ i ++ (tenga en cuenta que el código de operación incremental en cualquier caso devuelve un valor, incluso si no es necesario. Por lo tanto, se requiere una instrucción GRATUITA adicional para borrarlo).
10-11 : comprobación de la condición de salida después del incremento.

O aún puedes iterar


 $value = reset($arr); $key = key($arr); while($key !== null) { echo $key.$value; $value = next($arr); $key = key($arr); } 

Sábana debajo del alerón
 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 3 0 E > ASSIGN !0, <array> 5 1 INIT_FCALL 'reset' 2 SEND_REF !0 3 DO_ICALL $4 4 ASSIGN !1, $4 6 5 INIT_FCALL 'key' 6 SEND_VAR !0 7 DO_ICALL $6 8 ASSIGN !2, $6 7 9 > JMP ->20 8 10 > CONCAT ~8 !2, !1 11 ECHO ~8 9 12 INIT_FCALL 'next' 13 SEND_REF !0 14 DO_ICALL $9 15 ASSIGN !1, $9 10 16 INIT_FCALL 'key' 17 SEND_VAR !0 18 DO_ICALL $11 19 ASSIGN !2, $11 7 20 > IS_NOT_IDENTICAL ~13 !2, null 21 > JMPNZ ~13, ->10 11 22 > > RETURN 1 


Esta opción es buena porque es adecuada para iterar sobre una matriz con cualquier clave, y no solo con enteros monotónicamente crecientes.

Las funciones de reinicio , siguiente y clave son bastante ligeras, pero todavía hay algo de sobrecarga para llamarlas. Y, como veremos más adelante, estos costos son altos.

Aunque este enfoque es muy similar al principio de foreach , hay dos diferencias fundamentales entre ellos.

1) Mientras que reset , next y key (y current también) funcionan directamente con el puntero interno de una matriz, foreach usa su propio iterador y no cambia el estado del puntero interno.

Es decir

 foreach($arr as $v) echo $v.' - '.current($arr).PHP_EOL; //---------------- a - a b - a c - a 

2) Al usar foreach para iterar por valor, haga lo que haga con la matriz dentro del bucle, se iterará el conjunto de datos original

 foreach($arr as $v) { $arr = ['X','Y','Z']; echo $v.PHP_EOL; } var_dump($arr); //---------------- a b c array(3) { [0]=> string(1) "X" [1]=> string(1) "Y" [2]=> string(1) "Z" } 

Lo que sucederá al iterar sobre el enlace se puede leer en este RFC . No todo es muy simple allí.

array_walk con función anónima


 array_walk($arr, function($value, $key){ echo $key.$value;}); 

Como se utiliza una función personalizada, habrá un conjunto adicional de códigos de operación.

Función


 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 0 E > RECV !0 1 RECV !1 2 CONCAT ~2 !1, !0 3 ECHO ~2 4 > RETURN null 

Código principal


 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 2 0 E > ASSIGN !0, <array> 4 1 INIT_FCALL 'array_walk' 2 SEND_REF !0 3 DECLARE_LAMBDA_FUNCTION '%00%7Bclosure%7D%2Fin%2FHuNEK0x7f9fa62d105a' 4 SEND_VAL ~2 5 DO_ICALL 6 > RETURN 1 

Como array_walk , como el resto de las funciones de la biblioteca estándar, es intrínseco, no existe un mecanismo de iteración en los códigos de operación compilados.

INIT_FCALL : inicializa la llamada a array_walk
SEND_REF : coloca una referencia de matriz en la pila de llamadas
DECLARE_LAMBDA_FUNCTION : declarar una función anónima
SEND_VAL : colocamos una función anónima en la pila de llamadas
DO_ICALL : ejecuta array_walk para ejecutar

Luego, la magia ocurre con la llamada de nuestra lambda para cada elemento de la matriz.

array_walk usando una función predefinida


No es muy diferente de llamar con una anónima, excepto quizás un poco menos que la sobrecarga de crear una lambda en tiempo de ejecución.

 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 3 0 E > ASSIGN !0, <array> 9 2 INIT_FCALL 'array_walk' 3 SEND_REF !0 4 SEND_VAL 'my_echo' 5 DO_ICALL 6 > RETURN 1 

Las conclusiones son comunes. foreach está diseñado para iterar matrices, mientras que el resto de los bucles son solo una envoltura sobre if + goto .

Las funciones de la biblioteca estándar funcionan según el principio de un cuadro negro.

Bucear un poco más profundo


Primero, considere el caso con for y su código de operación FETCH_DIM_R , que se utiliza para extraer el valor por clave. El elemento se extrae mediante una búsqueda en la tabla hash ( ZEND_HASH_INDEX_FIND ). En nuestro caso, la extracción proviene de una matriz empaquetada (las teclas son una secuencia numérica continua); esta es una operación bastante fácil y rápida. Para las matrices desempaquetadas, será un poco más caro.

Ahora foreach ( FE_FETCH_R ). Todo es trivial aquí:

  1. Comprobando si el puntero iterador está fuera de la matriz
  2. Recuperar la clave y el valor actual
  3. Puntero de incremento

Bueno, ¿qué pasa con funciones como array_walk ? Aunque todos hacen cosas diferentes, todos tienen el mismo principio.

Si está completamente simplificado, entonces (pseudocódigo):

 reset_pointer() do { value = get_current_value() if (value == NULL) break //  NULL,   zval  NULL key = get_current_key() call_function(myFunction, key, value) increment_pointer() } while(true) 

De hecho, todo es más complicado por dentro, pero la esencia es la misma: hay una búsqueda bastante rápida de la tabla hash sin la participación de la máquina virtual PHP (sin tener en cuenta la llamada a la función definida por el usuario).

Bueno, algunas medidas


Y luego, después de todo, cuál es el artículo sin mediciones (de memoria resultó tan igualmente que eliminó su medición).

Como una matriz, según la tradición, tomamos zend_vm_execute.h para 70.108 líneas.

Script de medición de spoiler
 <?php $array = explode("\n", file_get_contents('/Users/rjhdby/CLionProjects/php-src/Zend/zend_vm_execute.h')); function myEcho($key, $value){ echo $key . $value; } ob_start(); $startTime = microtime(true); for ($i = 0; $i < 10; $i++) { // foreach ($array as $key => $value) { // echo $key . $value; // } // foreach ($array as $key => &$value) { // echo $key . $value; // } // $length = count($array); // for ($j = 0; $j < $length; $j++) { // echo $j . $array[$j]; // } // array_walk($array, function($value, $key) { // echo $key . $value; // }); // array_walk($array, static function($value, $key) { // echo $key . $value; // }); // array_walk($array, 'myEcho'); // $value = reset($array); // $key = key($array); // while($key !== null) { // echo $key.$value; // $value = next($array); // $key = key($array); // } } $endTime = microtime(true); ob_clean(); echo "time: " . ($endTime - $startTime); 


Cada medición se ejecutó 10 veces, eligiendo la más frecuente en los primeros 4 dígitos.

 foreach time: 0.12159085273743 foreach(reference) time: 0.11191201210022 for, while time: 0.13130807876587 array_walk(lambda) time: 0.87953400611877 array_walk(static lambda) time: 0.87544417497211 array_walk(name) time: 0.50753092765808 next,key time: 1.06258893013 

Para resumir


Analizando los resultados, no olvide tener en cuenta que se obtuvieron en 10 pases a través de una matriz de 70 mil elementos.

El antihéroe absoluto fue la "emulación" de foreach con next / key . No lo hagas a menos que sea absolutamente necesario.

array_walk con una lambda respira en su espalda, pero hay un matiz. El próximo JIT puede cambiar dramáticamente la situación. O tal vez no cambiar. Será interesante verlo.
array_walk usando una función preparada es un medio fuerte.

Dado que foreach funciona un poco diferente al iterar sobre un enlace (utiliza el código de operación FE_FETCH_RW en lugar de FE_FETCH_R ), realizó una medición por separado para él. Realmente resultó un poco más rápido.

Resultó que crear una lambda sobre la marcha no es la operación más barata. Parece que se crea solo 10 veces. Será necesario estudiar.

Todos los demás métodos mostraron aproximadamente los mismos resultados, con una brecha muy leve.

Gracias por su atencion!

Si tiene alguna sugerencia, ¿qué más puede "elegir"? Escriba en los comentarios. Todavía estoy pensando en lambdas: tal reducción de rendimiento es muy extraña.

UPD
Se agregó medición para array_walk con lambda estática. La diferencia no es visible.

Source: https://habr.com/ru/post/460285/


All Articles