Joy: Was ist los?
Traurigkeit: Wir abstrahieren! Es gibt vier Stufen. Dies ist der erste. Nicht objektive Fragmentierung!
Bing Bong: Okay, keine Panik. Wichtig ist, dass wir alle zusammen bleiben. [plötzlich fällt sein abstrakter Arm ab]
Joy: Oh! [Traurigkeit und Freude fallen auch auseinander]
Traurigkeit: Wir sind in der zweiten Phase. Wir dekonstruieren! [als Bing Bong in Stücke fällt]
Bing Bong: Ich kann meine Beine nicht fühlen! [nimmt ein Bein hoch] Oh, da sind sie.
© Cartoon von innen nach außenJeder liebt es, schönen Code zu schreiben. Zu Abstraktionen, Lambdas, SOLID, DRY, DI usw. usw. In diesem Artikel möchte ich untersuchen, wie viel es in Bezug auf die Leistung kostet und warum.
Dazu übernehmen wir eine einfache Aufgabe, die von der Realität getrennt ist, und wir werden nach und nach Schönheit in sie einbringen, die Leistung messen und unter die Haube schauen.
Haftungsausschluss: Dieser Artikel sollte in keiner Weise als Aufruf zum Schreiben von fehlerhaftem Code ausgelegt werden. Es ist am besten, wenn Sie im Voraus abstimmen und nach dem Lesen von „Cool! Jetzt weiß ich, wie es drinnen ist. Aber ich werde es natürlich nicht benutzen. “ :) :)
Herausforderung:
- Dan Textdatei.
- Wir brechen es in Linien.
- Schneiden Sie die Zwischenräume nach links und rechts
- Verwerfen Sie alle Leerzeilen.
- Wir ersetzen alle nicht einzelnen Leerzeichen durch einzelne ("ABC" -> "ABC").
- Zeilen mit mehr als 10 Wörtern werden entsprechend den Wörtern rückwärts gespiegelt ("An Bn Cn" -> "Cn Bn An").
- Wir zählen, wie oft jede Zeile vorkommt.
- Drucken Sie alle Zeilen, die mehr als N Mal vorkommen.
Als Eingabedatei nehmen wir traditionell php-src / Zend / zend_vm_execute.h für ~ 70.000 Zeilen.
Nehmen Sie zur Laufzeit PHP 7.3.6.
Schauen wir uns die kompilierten Opcodes hier an:
https://3v4l.org .
Die Messungen werden wie folgt durchgeführt:
Erster Ansatz, naiv
Schreiben wir einen einfachen Imperativcode:
$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; } }
Laufzeit ~ 0,148 s.
Alles ist einfach und es gibt nichts zu erzählen.
Der zweite Ansatz, prozedural
Wir überarbeiten unseren Code und nehmen die elementaren Aktionen in der Funktion heraus.
Wir werden versuchen, den Grundsatz der alleinigen Verantwortung einzuhalten.
Fußtuch unter dem 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 );
Laufzeit ~ 0,275s ... WTF !? Der Unterschied ist fast 2 mal!
Mal sehen, was eine PHP-Funktion aus Sicht einer virtuellen Maschine ist.
Code:
$a = 1; $b = 2; $c = $a + $b;
Kompiliert zu:
line
Lassen Sie uns den Zusatz in eine Funktion einfügen:
function sum($a, $b){ return $a + $b; } $a = 1; $b = 1; $c = sum($a, $b);
Dieser Code wird in zwei Gruppen von Opcodes kompiliert: einen für den Root-Namespace und einen für die Funktion.
Wurzel:
line
Funktion:
line
Das heißt, Selbst wenn Sie nur nach Opcodes zählen, fügt jeder Funktionsaufruf 3 + 2N Opcodes hinzu, wobei N die Anzahl der übergebenen Argumente ist.
Und wenn Sie etwas tiefer graben, dann wechseln wir hier auch den Ausführungskontext.
Eine grobe Schätzung unseres überarbeiteten Codes ergibt solche Zahlen (denken Sie an etwa 70.000 Iterationen).
Die Anzahl der "zusätzlich" ausgeführten Opcodes: ~ 17.000.000.
Anzahl der Kontextwechsel: ~ 280.000.
Der dritte Ansatz, klassisch
Besonders ohne zu philosophieren, wickeln wir all diese Funktionen in eine Klasse ein.
Bettlaken unter dem 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);
Vorlaufzeit: 0,297. Es wurde schlimmer. Nicht viel, aber auffällig. Ist die Erstellung eines Objekts (in unserem Fall 10-mal) so teuer? Nuuu ... Nicht nur das.
Mal sehen, wie eine virtuelle Maschine mit einer Klasse funktioniert.
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();
Es wird drei Sätze von Opcodes geben, was logisch ist: den Root und zwei Methoden.
Wurzel:
line
Konstruktor:
line
Summenmethode :
line
Das
neue Schlüsselwort wird tatsächlich in einen Funktionsaufruf konvertiert (Zeilen 3-6).
Es erstellt eine Instanz der Klasse und ruft den Konstruktor mit den übergebenen Parametern auf.
Im Methodencode werden wir daran interessiert sein, mit Klassenfeldern zu arbeiten. Bitte beachten Sie, dass, wenn Sie einen einfachen
ASSIGN- Opcode mit normalen Variablen für die Zuweisung zuweisen, für Klassenfelder alles etwas anders ist.
Zuordnung - 2 Opcodes
7 2 ASSIGN_OBJ 'a' 3 OP_DATA !0
Lesen - 1 Opcode
1 FETCH_OBJ_R ~1 'b'
Hier sollten Sie wissen, dass
ASSIGN_OBJ und
FETCH_OBJ_R viel komplizierter und dementsprechend ressourcenintensiver sind als ein einfaches
ASSIGN , das grob gesagt einfach
zval von einem Speicherelement in ein anderes kopiert.
Es ist klar, dass ein solcher Vergleich alles andere als korrekt ist, aber dennoch eine Vorstellung davon gibt. Ein wenig weiter werde ich Messungen vornehmen.
Nun wollen wir sehen, wie teuer es ist, ein Objekt zu instanziieren. Messen wir an einer Million Iterationen:
class ValueObject{ private $a; function __construct($a) { $this->a = $a; } } $start = microtime(true); for($i = 0; $i < 1000000; $i++){
Variablenzuordnung: 0,092.
Instanzobjekt: 0,889.
So etwas in der Art. Nicht ganz kostenlos, besonders wenn oft.
Um nicht zweimal aufzustehen, messen wir den Unterschied zwischen der Arbeit mit Eigenschaften und lokalen Variablen. Ändern Sie dazu unseren Code folgendermaßen:
class ValueObject{ private $b; function try($a) {
Austausch durch Zuordnung: 0,830.
Austausch durch Eigentum: 0,862.
Nur ein bisschen, aber länger. Genau die gleiche Reihenfolge der Unterschiede, die Sie nach dem Umschließen von Funktionen in eine Klasse erhalten haben.
Banale Schlussfolgerungen
- Wenn Sie das nächste Mal eine Million Objekte instanziieren möchten, überlegen Sie, ob Sie sie wirklich benötigen. Vielleicht nur ein Array, oder?
- Schreiben Sie einen Spaghetti-Code, um eine Millisekunde zu sparen. Der Auspuff ist billig und Kollegen können sie später schlagen.
- Aber um 500 Millisekunden einzusparen, ist es manchmal sinnvoll. Die Hauptsache ist, nicht zu weit zu gehen und sich daran zu erinnern, dass diese 500 Millisekunden höchstwahrscheinlich nur durch einen kleinen Abschnitt sehr heißen Codes gespeichert werden und nicht das gesamte Projekt in eine Leere der Trauer verwandeln.
PS Über Lambdas beim nächsten Mal. Es ist dort interessant. :) :)