
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):
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'];
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 = 04, 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;
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);
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_walkSEND_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í:
- Comprobando si el puntero iterador está fuera de la matriz
- Recuperar la clave y el valor actual
- 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
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++) {
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.
UPDSe agregó medición para
array_walk con lambda estática. La diferencia no es visible.