PGO für Datenbankserver richtet Code an den meistgenutzten Ausführungspfaden aus
PGO, die profilgesteuerte Optimierung der relationalen Open-Source-Datenbank, nutzt Erkenntnisse aus einem Trainingslauf, um zu verändern, wie Datenbanksoftware in ausführbare Maschinenbefehle übersetzt wird. In seiner Erläuterung des Verfahrens beschreibt Percona eine Technik, die mehr leistet, als die für das Training verwendeten Abfragen zu beschleunigen: Sie ordnet den Code im gesamten Datenbankprogramm neu an. Entscheidend für die Übereinstimmung mit dem Produktivbetrieb ist, welche Ausführungspfade häufig durchlaufen werden, und nicht, ob die Trainingsdaten dieselben Werte enthalten.
Perconas Darstellung ist der zweite Teil einer dreiteiligen Reihe, zwischen einer Einführung in PGO und einer Erörterung der Frage, warum ein mit dem Lasttestwerkzeug sysbench trainierter Build an Leistung verliert. Dieser Teil erläutert die Mechanismen, statt vergleichende Benchmark-Ergebnisse vorzulegen. Er liefert daher keine gemessene Geschwindigkeitssteigerung, von der Datenbankbetreiber annehmen können, dass sie auch für ihre eigene Installation gilt.
Zwei Builds mit einem Trainingslauf dazwischen
PGO beginnt mit der Kompilierung eines instrumentierten Builds, einer Programmversion mit zusätzlichen Zählern, die die Ausführungsaktivität aufzeichnen. Percona beschreibt, dass dafür die Compiler-Option profile-generation verwendet, eine Trainingslast ausgeführt und die Messwerte in Rohprofildateien geschrieben werden. Eine zweite Kompilierung nutzt die aufgezeichnete Aktivität, um zu entscheiden, wie das fertige Programm angeordnet und optimiert wird. Die genauen Einzelheiten der Profilerstellung hängen vom Compiler ab, dem Werkzeug, das Quellcode in Maschinenbefehle übersetzt.
Die nützlichen Erkenntnisse gehen über das bloße Zählen von Funktionsaufrufen hinaus, wobei eine Funktion ein wiederverwendbarer Codeabschnitt ist. Percona beschreibt Zähler an den Verbindungen im Kontrollflussgraphen, einer Übersicht der möglichen Wege durch ein Programm. Diese erfassen die Wahl von Verzweigungen, wiederholte Schleifendurchläufe und die Aktivität an Aufrufstellen, also den Stellen, an denen eine Funktion eine andere aufruft. Das veranschaulichende Beispiel unterscheidet einen 950.000-mal durchlaufenen Zweig von einer Alternative, die nur 50-mal durchlaufen wird. Diese Zahlen dienen der Erklärung und sind keine veröffentlichten Datenbank-Benchmark-Ergebnisse.
Inlining: Platz für Code dort einsetzen, wo er hilft
Inlining bedeutet, die Anweisungen einer Funktion direkt an ihrer Aufrufstelle einzusetzen, statt einen separaten Aufruf auszuführen. Ohne Erkenntnisse aus der Laufzeit stützen sich Compiler auf Regeln wie die Funktionsgröße, die Anzahl der Aufrufstellen sowie geschätzte Kosten und Nutzen. Profile können das Inlining eines millionenfach ausgeführten Aufrufs rechtfertigen, selbst wenn gewöhnliche Größenregeln dagegen sprechen würden. Umgekehrt kann ein Aufruf in selten ausgeführtem Code separat bleiben, weil das Kopieren seiner Anweisungen das Programm bei geringem Nutzen vergrößern würde.
Die Anordnung von Verzweigungen verändert, welcher Pfad direkt zu den nächsten Anweisungen führt und welcher einen Sprung an eine andere Stelle erfordert. Prozessoren sagen den Ausgang von Verzweigungen voraus, um Anweisungen vor ihrer Ausführung zu laden. Eine falsche Vorhersage zwingt sie, spekulativ ausgeführte Arbeit zu verwerfen, was einen kurzen Stillstand verursacht. Den häufig genutzten Pfad ohne Sprung weiterlaufen zu lassen, kann das Laden von Anweisungen erleichtern. Die tatsächlichen Kosten hängen jedoch vom Prozessor und davon ab, ob seine Vorhersage korrekt ist. Percona beschreibt, wie seltene Fehlerbehandlung aus diesem Pfad ausgelagert wird, oft in die unlikely-code-Sektion, einen Bereich der ausführbaren Datei, der selten verwendeten Anweisungen vorbehalten ist.
Die Reihenfolge einer if-else-Anweisung im Quellcode zu ändern, ist keine gleichwertige Optimierung. Der Compiler wählt die Anordnung des Maschinencodes anhand von Profilen oder eigenen Regeln. Die Reihenfolge, in der ein Entwickler die Alternativen schreibt, hat im Allgemeinen wenig oder gar keinen Einfluss auf die endgültige Anordnung.
Häufig verwendete Anweisungen zusammenhalten
Beim Hot/Cold Function Splitting werden häufig ausgeführte, sogenannte heiße Anweisungen von selten ausgeführten, sogenannten kalten Anweisungen innerhalb derselben Funktion getrennt. Eine stark genutzte Schleife kann in der hot-code-Sektion bleiben, einem Bereich für häufig verwendeten Code, während ihr selten benötigter Fehlerbehandlungsteil am Ende in die unlikely-code-Sektion wandert. Ein Sprung verbindet die Teile weiterhin, wenn dies nötig ist. Der kleinere heiße Teil nutzt den Befehlscache, den kleinen, schnellen Speicher des Prozessors für demnächst benötigte Anweisungen, besser aus, statt sich diesen Platz mit selten verwendetem Code zu teilen.
Eine Neuanordnung findet auch über ganze Funktionen hinweg statt, wenn die ausführbare Datei beim Linken zusammengesetzt wird. Laut Percona aktiviert der PGO-Build des Datenbankservers LTO, die Linkzeitoptimierung. Diese ermöglicht es der Werkzeugkette, über getrennt kompilierte Programmteile hinweg zu optimieren. Funktionen, die einander häufig aufrufen oder nacheinander ausgeführt werden, können nahe beieinander platziert werden. Das verbessert die Lokalität im Befehlscache und im iTLB: Der Instruction Translation Lookaside Buffer ist ein Prozessorcache, der dabei hilft, den Speicher für Anweisungen zu finden.
Percona beschreibt diese programmweite Neuanordnung als eine potenziell besonders große Verbesserung für die ausführbare Datei des Datenbankservers und spricht dabei von Verschiebungen innerhalb einer mehr als 100 MB großen Binärdatei. Die gewöhnliche Anordnung beim Erstellen eines Builds spiegelt nicht unbedingt die zur Laufzeit genutzten Pfade wider. Werden zusammengehörige, häufig ausgeführte Funktionen dicht beieinander angeordnet, müssen seltener Anweisungen von weit auseinanderliegenden Stellen geladen werden. Der Artikel beziffert diesen Vorteil jedoch nicht für eine bestimmte Einsatzumgebung.
Besser vorhersagbare Aufrufe und andere Entscheidungen zur Codegröße
Indirect-call promotion betrifft Aufrufe, deren Ziel während der Programmausführung ausgewählt wird, etwa Aufrufe über Funktionszeiger oder virtuelle Aufrufe in C++. Virtuelle Aufrufe verwenden üblicherweise vtables, also Tabellen, die die Implementierung einer Operation für ein Objekt auswählen. Percona nennt als Beispiel eine Storage-Engine-Schnittstelle, bei der faktisch nur die Datenspeicher-Engine der Datenbank registriert ist und deshalb ein Ziel dominieren kann. Der Compiler kann eine Prüfung einfügen: Stimmt das Ziel mit diesem häufigen Ziel überein, wird es direkt aufgerufen; andernfalls bleibt der ursprüngliche indirekte Aufruf erhalten. So bleibt die Ausweichmöglichkeit bestehen, während das häufige Ziel leichter vorherzusagen ist.
Profile beeinflussen auch die Registerzuweisung, also die Zuordnung von Werten zum schnellsten Arbeitsspeicher des Prozessors, sowie das Schleifenentrollen, bei dem Schleifenanweisungen wiederholt werden, um den Aufwand für die Schleifensteuerung zu verringern. Bei häufig ausgeführten Pfaden kann die Geschwindigkeit Vorrang haben, mit stärkerem Entrollen und mehr Registern für häufig benötigte Werte. Bei selten ausgeführten Pfaden kann Kompaktheit wichtiger sein. Percona erwähnt in diesem Zusammenhang die Compiler-Option partial-profile-training; wie nicht trainierter Code behandelt wird, hängt von den Compiler-Einstellungen ab. Selten verwendete Anweisungen beanspruchen weiterhin Speicherplatz auf dem Datenträger und im Seitencache, einem Speicher für Dateiinhalte. Daher sind ihre Größe und Platzierung auch dann wichtig, wenn sie nur selten ausgeführt werden.
Eine switch-Anweisung, die zwischen mehreren Alternativen auswählt, erfordert eine weitere Abwägung. Ein Compiler kann eine Sprungtabelle verwenden, eine Nachschlagestruktur, die eine Auswahl mit konstanter Laufzeit, also in O(1), ermöglicht, aber einen Tabellenzugriff und einen indirekten Sprung erfordert. Alternativ kann er aufeinanderfolgende Vergleiche erzeugen. Erkenntnisse aus dem Profil können Vergleiche attraktiv machen, wenn ein Fall dominiert und vorhersagbar ist, statt alle Fälle als gleich wahrscheinlich zu behandeln.