Alegria: O que está acontecendo?
Tristeza: Estamos abstraindo! Existem quatro estágios. Este é o primeiro. Fragmentação não objetiva!
Bing Bong: Tudo bem, não entre em pânico. O importante é que todos fiquemos juntos. [de repente seu braço abstrato cai]
Alegria: Oh! [Tristeza e alegria também começam a desmoronar]
Tristeza: Estamos no segundo estágio. Estamos desconstruindo! [como Bing Bong cai em pedaços]
Bing Bong: Não consigo sentir minhas pernas! [pega uma perna] Oh, lá estão eles.
© Cartoon Inside OutTodo mundo gosta de escrever um código bonito. Para abstrações, lambdas, SÓLIDO, SECO, DI, etc. etc. Neste artigo, quero explorar quanto custa em termos de desempenho e por quê.
Para fazer isso, assumimos uma tarefa simples, divorciada da realidade, e gradualmente trazemos beleza para ela, medindo o desempenho e olhando sob o capô.
Isenção de responsabilidade: este artigo não deve ser interpretado como uma chamada para escrever código incorreto. É melhor que você sintonize com antecedência para dizer depois de ler “Legal! Agora eu sei como é por dentro. Mas é claro que não vou usá-lo. :)
Desafio:
- Dan arquivo de texto.
- Nós dividimos em linhas.
- Aparar os espaços esquerdo e direito
- Descarte todas as linhas em branco.
- Substituímos todos os espaços não individuais por espaços únicos ("ABC" -> "ABC").
- As linhas com mais de 10 palavras, de acordo com as palavras, são viradas para trás ("An Bn Cn" -> "Cn Bn An").
- Contamos quantas vezes cada linha ocorre.
- Imprima todas as linhas que ocorrem mais de N vezes.
Como um arquivo de entrada, por tradição tomamos o php-src / Zend / zend_vm_execute.h por ~ 70 mil linhas.
Como tempo de execução, use o PHP 7.3.6.
Vejamos os opcodes compilados aqui
https://3v4l.org .
As medições serão feitas da seguinte forma:
Primeira abordagem, ingênua
Vamos escrever um código imperativo simples:
$array = explode("\n", file_get_contents('/Users/rjhdby/CLionProjects/php-src/Zend/zend_vm_execute.h')); $cache = []; foreach ($array as $row) { if (empty($row)) continue; $words = preg_split("/\s+/", trim($row)); if (count($words) > 10) { $words = array_reverse($words); } $row = implode(" ", $words); if (isset($cache[$row])) { $cache[$row]++; } else { $cache[$row] = 1; } } foreach ($cache as $key => $value) { if ($value > 1000) { echo "$key : $value" . PHP_EOL; } }
Tempo de execução ~ 0,148 s.
Tudo é simples e não há nada para falar.
A segunda abordagem, processual
Nós refatoramos nosso código e executamos as ações elementares na função.
Vamos tentar aderir ao princípio da responsabilidade exclusiva.
Calçado sob o spoiler. function getContentFromFile(string $fileName): array { return explode("\n", file_get_contents($fileName)); } function reverseWordsIfNeeded(array &$input) { if (count($input) > 10) { $input = array_reverse($input); } } function prepareString(string $input): string { $words = preg_split("/\s+/", trim($input)); reverseWordsIfNeeded($words); return implode(" ", $words); } function printIfSuitable(array $input, int $threshold) { foreach ($input as $key => $value) { if ($value > $threshold) { echo "$key : $value" . PHP_EOL; } } } function addToCache(array &$cache, string $line) { if (isset($cache[$line])) { $cache[$line]++; } else { $cache[$line] = 1; } } function processContent(array $input): array { $cache = []; foreach ($input as $row) { if (empty($row)) continue; addToCache($cache, prepareString($row)); } return $cache; } printIfSuitable( processContent( getContentFromFile('/Users/rjhdby/CLionProjects/php-src/Zend/zend_vm_execute.h') ), 1000 );
Tempo de execução ~ 0,275s ... WTF!? A diferença é quase 2 vezes!
Vamos ver o que é uma função PHP do ponto de vista de uma máquina virtual.
Código:
$a = 1; $b = 2; $c = $a + $b;
Compila para:
line
Vamos colocar a adição em uma função:
function sum($a, $b){ return $a + $b; } $a = 1; $b = 1; $c = sum($a, $b);
Esse código é compilado em dois conjuntos de códigos de operação: um para o namespace raiz e o segundo para a função.
Raiz:
line
Função:
line
I.e. mesmo se você apenas contar por códigos de operação, cada chamada de função adicionará 3 + 2N códigos de operação, onde N é o número de argumentos passados.
E se você se aprofundar um pouco, aqui também mudamos o contexto de execução.
Uma estimativa aproximada do nosso código refatorado fornece esses números (lembre-se de cerca de 70.000 iterações).
O número de códigos de operação executados "adicionais": ~ 17.000.000.
Número de opções de contexto: ~ 280.000.
A terceira abordagem, clássica
Especialmente sem filosofar, envolvemos todas essas funções com uma classe.
Lençol sob o spoiler class ProcessFile { private $content; private $cache = []; function __construct(string $fileName) { $this->content = explode("\n", file_get_contents($fileName)); } private function reverseWordsIfNeeded(array &$input) { if (count($input) > 10) { $input = array_reverse($input); } } private function prepareString(string $input): string { $words = preg_split("/\s+/", trim($input)); $this->reverseWordsIfNeeded($words); return implode(" ", $words); } function printIfSuitable(int $threshold) { foreach ($this->cache as $key => $value) { if ($value > $threshold) { echo "$key : $value" . PHP_EOL; } } } private function addToCache(string $line) { if (isset($this->cache[$line])) { $this->cache[$line]++; } else { $this->cache[$line] = 1; } } function processContent() { foreach ($this->content as $row) { if (empty($row)) continue; $this->addToCache( $this->prepareString($row)); } } } $processFile = new ProcessFile('/Users/rjhdby/CLionProjects/php-src/Zend/zend_vm_execute.h'); $processFile->processContent(); $processFile->printIfSuitable(1000);
Prazo de execução: 0,297. Ficou pior. Não muito, mas perceptível. A criação de um objeto (10 vezes no nosso caso) é tão cara? Nuuu ... Não é só isso.
Vamos ver como uma máquina virtual funciona com uma classe.
class Adder{ private $a; private $b; function __construct($a, $b) { $this->a = $a; $this->b = $b; } function sum(){ return $this->a + $this->b; } } $a = 1; $b = 1; $adder = new Adder($a, $b); $c = $adder->sum();
Haverá três conjuntos de códigos de operação, o que é lógico: a raiz e dois métodos.
Raiz:
line
Construtor:
line
Método de
soma :
line
A
nova palavra-chave é realmente convertida em uma chamada de função (linhas 3-6).
Ele cria uma instância da classe e chama o construtor com os parâmetros passados.
No código de métodos, estaremos interessados em trabalhar com campos de classe. Observe que, se você atribuir um código de operação
ASSIGN simples com variáveis comuns para atribuição, então para os campos de classe tudo será um pouco diferente.
Tarefa - 2 opcodes
7 2 ASSIGN_OBJ 'a' 3 OP_DATA !0
Leitura - 1 opcode
1 FETCH_OBJ_R ~1 'b'
Aqui você deve saber que
ASSIGN_OBJ e
FETCH_OBJ_R são muito mais complicados e, portanto,
consomem mais recursos do que um
ASSIGN simples, que, grosso modo, simplesmente copia o
zval de uma parte da memória para outra.
É claro que essa comparação está muito longe de ser correta, mas ainda dá uma idéia. Um pouco mais adiante, farei medições.
Agora vamos ver como é caro instanciar um objeto. Vamos medir em um milhão de iterações:
class ValueObject{ private $a; function __construct($a) { $this->a = $a; } } $start = microtime(true); for($i = 0; $i < 1000000; $i++){
Atribuição de variável: 0,092.
Objeto da instância: 0,889.
Algo assim. Não é totalmente gratuito, especialmente se muitas vezes.
Bem, para não levantar duas vezes, vamos medir a diferença entre trabalhar com propriedades e variáveis locais. Para fazer isso, altere nosso código da seguinte maneira:
class ValueObject{ private $b; function try($a) {
Troca por atribuição: 0,830.
Troca através da propriedade: 0,862.
Só um pouco, mas mais. Exatamente a mesma ordem de diferença que você obteve após agrupar as funções em uma classe.
Conclusões banais
- Na próxima vez que você quiser instanciar um milhão de objetos, pense se realmente precisa dele. Talvez apenas uma matriz, hein?
- Escrever um código de espaguete para economizar um milissegundo - bem, isso. O escape é barato, e os colegas podem vencê-los mais tarde.
- Mas, para economizar 500 milissegundos, talvez às vezes faça sentido. O principal é não ir muito longe e lembre-se de que esses 500 milissegundos provavelmente serão salvos apenas por uma pequena seção de código muito quente, e não transformar o projeto inteiro em um vazio de tristeza.
PS Sobre lambdas da próxima vez. É interessante lá. :)