Verspotte den fröhlichen Branch Predictor nicht
- In letzter Zeit wird viel AArch64-Assembly geschrieben
- Eine „clevere“ Idee, in einer Schleife einen Sprung zu entfernen, verschlechtert die Performance
- Dieser Fehler wird erklärt, damit andere ihn nicht ebenfalls machen
Codebeispiel
float run(const float* data, size_t n) {
float g = 0.0;
while (n) {
n--;
const float f = *data++;
foo(f, &g);
}
return g;
}
static void foo(float f, float* g) {
// g를 수정하는 작업
}
Übersetzung in AArch64-Assembly
// x0: const float* data
// x1: size_t n
// s0: zurückzugebender float
stp x29, x30, [sp, #-16]!
mov s0, #0.0
loop:
cmp x1, #0
b.eq exit
sub x1, x1, #1
ldr s1, [x0], #4
bl foo
b loop
foo:
// aus s1 lesen und in s0 akkumulieren
// ...
ret
exit:
ldp x29, x30, [sp], #16
ret
Optimierungsversuch
- Die Anzahl der
bl-Instruktionen sollte reduziert werden, um die Performance zu verbessern
- Stattdessen wurde die Performance schlechter
Performance-Vergleich
- Originalcode: 969 ns
- Optimierter Code: 3.85 µs
Ursachenanalyse
- Der Branch Predictor wird verwirrt, weil
bl- und ret-Paare nicht zusammenpassen
- Laut der ARM-Dokumentation hilft die
ret-Instruktion bei der Vorhersage von Funktionsrückgaben
Lösung
br x30 statt ret verwenden
- Performance wiederhergestellt: 913 ns
Weitere Optimierung
foo inline setzen, um die Performance zu verbessern
- Loop-Unrolling und SIMD-Instruktionen verwenden
Endgültige Performance
- SIMD + manuelles Loop-Unrolling: 94 ns
Fazit
- Den Branch Predictor nicht verwirren
- SIMD-Code ist schneller, aber da Gleitkommaaddition nicht assoziativ ist, können die Ergebnisse unterschiedlich sein
Meinung von GN⁺
- Dieser Artikel zeigt die Bedeutung von AArch64-Assembly-Optimierung sehr gut
- Das Verständnis der Funktionsweise des Branch Predictors ist für Performance-Optimierung unerlässlich
- Optimierungen mit SIMD-Instruktionen sind sehr effektiv, aber Genauigkeitsprobleme müssen berücksichtigt werden
- Mit High-Level-Sprachen wie Rust lässt sich die Performance durch Compiler-Optimierungen leichter verbessern
- Ein ähnliches Projekt ist Agner Fogs Leitfaden zur Assembly-Optimierung
1 Kommentare
Hacker-News-Kommentar
Hat den Artikel zusammen mit Freunden aus der Apple-II-Ära zusammengefasst
Raymond Chen hat dasselbe Thema schon vor fast 20 Jahren behandelt
fooSIMD-Code kann Summen in anderer Reihenfolge ausführen, weil Gleitkommaaddition nicht assoziativ ist
Seit Rust 1.78 verwendet der Compiler aggressiveres Loop-Unrolling und etwas SIMD
Es gab Verwirrung darüber, wie sich
x0in ARM/ARM64-Assembly erhöhtldr s1, [x0], #4lädt und erhöht dabeix0um 4Es war überraschend, dass zum Optimieren des Assembly-Codes keine weniger komplexen Methoden ausprobiert wurden
fooinlinen und dieRET-Anweisung weglassenEs gab den Kommentar, dass der Autor nicht ständig die Einheiten hätte wechseln sollen