Alegría: ¿Qué está pasando?
Tristeza: ¡Estamos abstrayendo! Hay cuatro etapas. Este es el primero. ¡Fragmentación no objetiva!
Bing Bong: Muy bien, no entres en pánico. Lo importante es que todos estemos juntos. [de repente su brazo abstracto se cae]
Alegría: ¡Oh! [La tristeza y la alegría comienzan a desmoronarse también]
Tristeza: estamos en la segunda etapa. Estamos deconstruyendo! [como Bing Bong se cae a pedazos]
Bing Bong: ¡No puedo sentir mis piernas! [levanta una pierna] Oh, ahí están.
© Cartoon Inside OutA todos les encanta escribir código hermoso. A abstracciones, lambdas, SÓLIDAS, SECAS, DI, etc. etc. En este artículo, quiero explorar cuánto cuesta en términos de rendimiento y por qué.
Para hacer esto, tome una tarea simple que esté divorciada de la realidad y que gradualmente le aportará belleza, midiendo el rendimiento y mirando debajo del capó.
Descargo de responsabilidad: este artículo no debe interpretarse de ninguna manera como una llamada para escribir un código incorrecto. Es mejor si sintoniza con anticipación para decir después de leer "¡Genial! Ahora sé cómo es por dentro. Pero, por supuesto, no lo usaré ". :)
Desafío:
- Dan archivo de texto.
- Lo dividimos en líneas.
- Recorta los espacios a izquierda y derecha
- Deseche todas las líneas en blanco.
- Reemplazamos todos los espacios que no sean individuales con espacios individuales ("ABC" -> "ABC").
- Las líneas con más de 10 palabras, de acuerdo con las palabras, se vuelven hacia atrás ("An Bn Cn" -> "Cn Bn An").
- Contamos cuántas veces ocurre cada fila.
- Imprima todas las líneas que ocurren más de N veces.
Como un archivo de entrada, por tradición tomamos php-src / Zend / zend_vm_execute.h por ~ 70 mil líneas.
Como tiempo de ejecución, tome PHP 7.3.6.
Veamos los códigos de operación compilados aquí
https://3v4l.org .
Las mediciones se realizarán de la siguiente manera:
Primer acercamiento ingenuo
Escribamos un código imperativo simple:
$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; } }
Tiempo de ejecución ~ 0.148 s.
Todo es simple y no hay nada de qué hablar.
El segundo enfoque, procesal
Refactorizamos nuestro código y sacamos las acciones elementales en la función.
Intentaremos adherirnos al principio de responsabilidad exclusiva.
Paño debajo del alerón. 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 );
Tiempo de ejecución ~ 0.275s ... ¿WTF? ¡La diferencia es casi 2 veces!
Veamos qué es una función PHP desde el punto de vista de una máquina virtual.
Código:
$a = 1; $b = 2; $c = $a + $b;
Compila a:
line
Pongamos la adición en una función:
function sum($a, $b){ return $a + $b; } $a = 1; $b = 1; $c = sum($a, $b);
Dicho código se compila en dos conjuntos de códigos de operación: uno para el espacio de nombres raíz y el segundo para la función.
Raíz:
line
Función:
line
Es decir incluso si solo cuenta por códigos de operación, cada llamada de función agrega 3 + 2N códigos de operación, donde N es el número de argumentos pasados.
Y si profundizas un poco más, aquí también cambiamos el contexto de ejecución.
Una estimación aproximada de nuestro código refactorizado da tales números (recuerde unas 70,000 iteraciones).
El número de códigos de operación ejecutados "adicionales": ~ 17,000,000.
Número de cambios de contexto: ~ 280,000.
El tercer enfoque, clásico
Especialmente sin filosofar, envolvemos todas estas funciones con una clase.
Sábana debajo del alerón 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);
Plazo de ejecución: 0.297. Se puso peor. No mucho, pero notable. ¿La creación de un objeto (10 veces en nuestro caso) es tan costosa? Nuuu ... No solo eso.
Veamos cómo funciona una máquina virtual con una clase.
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();
Habrá tres conjuntos de códigos de operación, lo cual es lógico: la raíz y dos métodos.
Raíz:
line
Constructor:
line
Método de
suma :
line
La
nueva palabra clave se convierte realmente en una llamada de función (líneas 3-6).
Crea una instancia de la clase y llama al constructor con los parámetros pasados.
En el código de métodos, nos interesará trabajar con campos de clase. Tenga en cuenta que si asigna un código de operación
ASSIGN simple con variables ordinarias para la asignación, entonces, para los campos de clase, todo es algo diferente.
Asignación - 2 códigos de operación
7 2 ASSIGN_OBJ 'a' 3 OP_DATA !0
Leer - 1 código de operación
1 FETCH_OBJ_R ~1 'b'
Aquí debe saber que
ASSIGN_OBJ y
FETCH_OBJ_R son mucho más complicados y, en consecuencia,
requieren más recursos que un simple
ASSIGN , que, en términos generales, simplemente copia
zval de una pieza de memoria a otra.
Está claro que tal comparación está muy lejos de ser correcta, pero aún da alguna idea. Un poco más adelante haré mediciones.
Ahora veamos lo caro que es crear una instancia de un objeto. Midamos en un millón de iteraciones:
class ValueObject{ private $a; function __construct($a) { $this->a = $a; } } $start = microtime(true); for($i = 0; $i < 1000000; $i++){
Asignación variable: 0.092.
Objeto de instancia: 0.889.
Algo como esto No es completamente gratis, especialmente si muchas veces.
Bueno, para no levantarse dos veces, midamos la diferencia entre trabajar con propiedades y variables locales. Para hacer esto, cambie nuestro código de esta manera:
class ValueObject{ private $b; function try($a) {
Intercambio mediante asignación: 0.830.
Intercambio a través de propiedad: 0.862.
Solo un poco, pero por más tiempo. Justo el mismo orden de diferencia que obtuviste después de ajustar las funciones en una clase.
Conclusiones banales
- La próxima vez que desee crear una instancia de un millón de objetos, piense si realmente lo necesita. Tal vez solo una serie, ¿eh?
- Escribir un código de espagueti para ahorrar un milisegundo, bueno, eso. El escape es barato y los colegas pueden vencerlos más tarde.
- Pero en aras de ahorrar 500 milisegundos, tal vez a veces tenga sentido. Lo principal es no ir demasiado lejos y recordar que estos 500 milisegundos tienen más probabilidades de ser guardados solo por una pequeña sección de código muy activo, y no convertir todo el proyecto en un vacío de tristeza.
PD: sobre lambdas la próxima vez. Es interesante allí. :)