Estamos preparando o PHP. Como estão while, foreach, array_walk e algumas outras palavras assustadoras



Era noite, não havia o que fazer. É hora de organizar uma pequena análise de como diferem internamente alguns métodos de classificação de matrizes no PHP.

Fontes da ramificação principal (agora é 7.4, intercalada com 8)
O gerador opcode do php 7.3.0.
As medições foram feitas em 7.3.6.

Isenção de responsabilidade para nerds: mencionar alguns nanossegundos e ciclos de processador é um truque polêmico chamado "hipérbole".

Talvez, de fato, existam dezenas ou centenas de nanossegundos e milhares de medidas, mas ainda é tão pequeno que a necessidade de economizar neles sugere que algo no seu código esteja errado.

Fase de compilação


for , foreach , do e while são palavras-chave do idioma, enquanto funções array_ * são funções da biblioteca padrão. Portanto, ceteris paribus, de acordo com o primeiro, o analisador executará alguns nanossegundos mais rapidamente.

Analisador


Até o token da instrução , o caminho será o mesmo para todos

start -> top_statement_list -> top_statement -> statement 

Os loops são definidos no nível da instrução :
 ->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 

A diferença entre for_exprs e apenas expr é apenas a do primeiro, é permitido escrever vários expr , separados por vírgulas.

foreach_variable é uma construção que, além de apenas variável , também rastreia a descompressão usando list ou [] .

for_statement , foreach_statement , while_statement diferem da instrução padrão na medida em que adicionam a capacidade de analisar sintaxe alternativa:

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

A chamada de função está enterrada muito mais profundamente:

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

callable_variable , hmm ... Engraçado, não é? :)

Vamos passar para os opcodes.


Como exemplo, vamos fazer uma iteração simples de uma matriz indexada com a impressão de cada chave e valor. É claro que o uso de for , while e do para uma tarefa não se justifica, mas nosso objetivo é simplesmente mostrar o 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 

O que está acontecendo aqui:


FE_RESET_R : cria um iterador de $ 4 para a matriz ! 0 . Ou, se a matriz estiver vazia, ela prosseguirá imediatamente para a instrução 7 .
FE_FETCH_R : executa uma etapa de iteração, recupera a chave atual em ~ 5 e o valor em ! 1 . Ou, se o final da matriz for atingido, ele prosseguirá para a instrução 7 .
As instruções 3-6 não são particularmente interessantes. Aqui há uma conclusão e retorne para FE_FETCH_R .
FE_FREE : destrói o iterador.

por enquanto, ...


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

Este é realmente um caso especial de enquanto

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

Este é realmente um caso especial de if + goto

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

Os códigos de operação para todos os três casos serão quase idênticos. A menos que no caso de se , JMPNZ mudará para um par de JMPZ + JMP devido à entrada no corpo de se .
Para o loop do, os códigos de operação variam um pouco devido à sua natureza pós-verificação.

 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 esperado, há mais códigos de operação.
0-2 : calculando $ length .
3: $ i = 0
4, 10, 11 : verificação inicial da condição de saída e, de fato, a saída ou a transição para o corpo do ciclo.
5 (FETCH_DIM_R): $ arr [$ i]
6-7 : conclusão
8-9: $ i ++ (observe que o opcode de incremento, em qualquer caso, retorna um valor, mesmo que não seja necessário. Portanto, é necessária uma instrução FREE adicional para limpá-lo).
10-11 : verificando a condição de saída após o incremento.

Ou você ainda pode iterar


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

Lençol assim sob o spoiler
 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 


Essa opção é boa porque é adequada para iterar sobre uma matriz com qualquer chave, e não apenas com números inteiros crescentes monotonicamente.

As funções de redefinição , próxima e principal são bem leves, mas ainda há alguma sobrecarga para chamá-las. E, como veremos mais adiante, esses custos são altos.

Embora essa abordagem seja muito semelhante ao princípio do foreach , existem duas diferenças fundamentais entre elas.

1) Enquanto reset , next e key (e atual também) trabalham diretamente com o ponteiro interno de uma matriz, o foreach usa seu próprio iterador e não altera o estado do ponteiro interno.

I.e.

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

2) Ao usar o foreach para iterar por valor, faça o que fizer com a matriz dentro do loop, o conjunto de dados original será iterado

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

O que acontecerá quando a iteração sobre o link puder ser lida neste RFC . Tudo não é muito simples lá.

array_walk com função anônima


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

Como uma função personalizada é usada, haverá um conjunto adicional de códigos de operação.

Função


 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 o restante das funções da biblioteca padrão, é intrínseco, não há mecanismo de iteração nos códigos de operação compilados.

INIT_FCALL : inicialize a chamada para array_walk
SEND_REF : coloca uma referência de matriz na pilha de chamadas
DECLARE_LAMBDA_FUNCTION : declarar uma função anônima
SEND_VAL : colocamos uma função anônima na pilha de chamadas
DO_ICALL : execute array_walk para executar

Em seguida, a mágica acontece com a chamada do nosso lambda para cada elemento da matriz.

array_walk usando uma função predefinida


Não é muito diferente de chamar com um anônimo, exceto talvez um pouco menos do que a sobrecarga de criar um lambda em tempo de execução.

 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 

As conclusões são comuns. o foreach é adaptado para arrumar iterações, enquanto o restante dos loops é apenas um invólucro sobre if + goto .

As funções da biblioteca padrão funcionam no princípio de uma caixa preta.

Mergulhando um pouco mais fundo


Primeiro, considere o caso com for e seu código de operação FETCH_DIM_R , que é usado para extrair o valor pela chave. O item é extraído por meio de uma pesquisa na tabela de hash ( ZEND_HASH_INDEX_FIND ). No nosso caso, a extração vem de uma matriz compactada (as teclas são uma sequência numérica contínua) - essa é uma operação bastante fácil e rápida. Para matrizes descompactadas, será um pouco mais caro.

Agora foreach ( FE_FETCH_R ). Tudo é banal aqui:

  1. Verificando se o ponteiro do iterador está fora da matriz
  2. Recupere a chave e o valor atuais
  3. Ponteiro de incremento

Bem, o que acontece com funções como array_walk ? Embora todos façam coisas diferentes, todos eles têm o mesmo princípio.

Se completamente simplificado, então (pseudo-có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 fato, tudo é mais complicado por dentro, mas a essência é a mesma - há uma pesquisa rápida na tabela de hash sem a participação da máquina virtual PHP (sem levar em conta a chamada para a função de usuário).

Bem, algumas medidas


E, afinal, qual é o artigo sem medições (da memória, ficou tão igualmente que removeu sua medição).

Como uma matriz, de acordo com a tradição, usamos zend_vm_execute.h para 70.108 linhas.

Script de medição 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 medição foi executada 10 vezes, escolhendo a ocorrência mais frequente nos 4 primeiros 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 

Resumir


Analisando os resultados, não se esqueça de que eles foram obtidos em 10 passagens por uma matriz de 70 mil elementos.

O anti-herói absoluto foi a “emulação” de foreach com next / key . Não faça isso a menos que seja absolutamente necessário.

array_walk com uma lambda respira nas costas, mas há uma nuance. O próximo JIT pode mudar drasticamente a situação. Ou talvez não mude. Será interessante ver.
array_walk usando uma função pronta é um meio-termo forte.

Como o foreach funciona de maneira um pouco diferente ao iterar em um link (ele usa o opcode FE_FETCH_RW em vez de FE_FETCH_R ), ele fez uma medição separada para ele. Ele realmente saiu um pouco mais rápido.

Como se viu, criar um lambda em tempo real não é a operação mais barata. Parece que ele é criado apenas 10 vezes. Será necessário estudar.

Todos os outros métodos apresentaram aproximadamente os mesmos resultados, com uma diferença muito pequena.

Obrigado pela atenção!

Se você tiver alguma sugestão, o que mais você pode "escolher" - escreva nos comentários. Ainda estou pensando em lambdas - um rebaixamento de desempenho é muito estranho.

UPD
Adicionada medição para array_walk com lambda estático. A diferença não é visível.

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


All Articles