
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):
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'];
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 = 04, 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;
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);
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_walkSEND_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:
- Verificando se o ponteiro do iterador está fora da matriz
- Recupere a chave e o valor atuais
- 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
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++) {
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.
UPDAdicionada medição para
array_walk com lambda estático. A diferença não é visível.