HN vorgestellt: Notizen zu Go Plan9
(pehringer.info)Nutzung von Go für Concurrency und Parallelität
- Vorstellung eines Projekts, das die Rechenleistung für numerische Berechnungen durch die Nutzung von Concurrency und Parallelität in Go verbessern soll.
- Mit SIMD- (Same Instruction Multiple Data) Befehlen lassen sich Berechnungen auf Hardware-Ebene parallel ausführen.
- Da der Go-Compiler SIMD nicht nutzt und kein geeignetes allgemeines SIMD-Paket gefunden wurde, wurde beschlossen, ein eigenes Paket zu entwickeln.
Plan9-Assemblersprache
- Go verwendet mit Plan9 eine eigene Assemblersprache, die die Befehle und Register einer bestimmten Plattform leicht angepasst übernimmt.
- x86-Plan9 und ARM-Plan9 unterscheiden sich voneinander.
- Anhand eines einfachen Plan9-Beispiels werden die grundlegenden Verwendungsweisen erklärt.
Plan9-Beispiel
- Anhand der Dateien
AddInts_amd64.sundmain.gowerden die grundlegende Funktionsdeklaration und Verwendung in Plan9 erklärt. - Es wird erläutert, wie Funktionsargumente und Rückgabewerte gemäß den Aufrufkonventionen von Go auf dem Stack gespeichert werden.
Geplantes Paketdesign
- Es wird ein Paket entworfen, das eine schlanke Abstraktionsschicht für SIMD-Operationen bei Arithmetik und Bitoperationen bereitstellt.
- Dafür wird ein internes Paket mit architekturspezifischen Plan9-Implementierungen erstellt und über eine Initialisierungsfunktion eingerichtet.
SIMD-Beispiel
- Anhand eines Beispiels für eine x86-SIMD-Plan9-Funktion wird die Verwendung von SIMD erklärt.
- Mit den Dateien
Supported_amd64.sundAddFloat32_amd64.swird gezeigt, wie sich SSE-Unterstützung prüfen und eine Additionsoperation fürfloat32ausführen lässt.
Leistung und Ausblick
- Ein Diagramm zum Leistungsunterschied zwischen der Go-Softwareimplementierung und der Plan9-SIMD-Implementierung zeigt einen Geschwindigkeitsgewinn von etwa 200–450 %.
- Es wird gehofft, dass diese Notiz zu Projekten mit Plan9 und SIMD inspiriert.
# GN⁺-Zusammenfassung
- Dieser Artikel stellt vor, wie sich die Leistung durch die Nutzung von Concurrency und Parallelität in Go maximieren lässt.
- Es wird erklärt, wie sich mit der Plan9-Assemblersprache und SIMD-Befehlen Berechnungen auf Hardware-Ebene parallel ausführen lassen.
- Der Artikel zeigt Go-Entwicklern die Einsatzmöglichkeiten von Plan9 und SIMD auf und kann nützlich sein, um neue Ansätze zur Leistungssteigerung zu erkunden.
- Als Projekte mit ähnlicher Funktionalität werden SIMD-Unterstützungsbibliotheken für Rust oder SIMD-bezogene Bibliotheken für C++ empfohlen.
1 Kommentare
Hacker-News-Kommentare
Ein paar Anmerkungen zu Go-Assembly: Auf amd64 sind diese
inttatsächlich 64 Bit groß.Wenn man
int32verwendet, wird die Parameterliste zwar wortausgerichtet, aber es gibt einen Haken: Auf 64-Bit-Systemen beginnen Rückgabewerte immer an einem Doubleword-ausgerichteten Offset.NOSPLITist intextflag.hdefiniert, das vom Go-Compiler automatisch bereitgestellt wird. Soweit ich es gelesen habe, wirdNOSPLITallerdings wohl nur beiruntime.XX-Funktionen beachtet, tut hier also nichts und ist auch nicht nötig.NOSPLITbedeutet, dass der Compiler keinen Code einfügen soll, der prüft, ob der Stack überlaufen könnte und deshalb gesplittet werden muss. Für Funktionen, die keinen Stack-Speicher benötigen, ist es technisch unnötig; im Grunde existiert es, um zu verhindern, dass genau dieser Prüfcode in die Funktion injiziert wird, die selbst die Stack-Splitting-Prüfung durchführt.Falls sich jemand fragt:
4steht für „NOSPLIT“, und aus irgendeinem Grund heißt es, das sei nötig. Normalerweise folgt nach der Frame-Größe (dem Parameter nachNOSPLIT) die Argumentgröße, getrennt durch ein Minuszeichen.Das ist keine Subtraktion, sondern nur eine eigenartige Syntax. Eine Frame-Größe von
$24-8bedeutet, dass die Funktion einen 24-Byte-Frame hat und mit 8 Byte Argumenten im Caller-Frame aufgerufen wird.Wenn bei
TEXTkeinNOSPLITangegeben ist, muss die Argumentgröße zwingend angegeben werden. Bei Assembly-Funktionen mit Go-Prototyp prüftgo vet, ob die Argumentgröße korrekt ist.Quelle: https://go.dev/doc/asm
Es heißt: „Go verwendet eine eigene interne Assembly-Sprache namens Plan9“ – wird diese Sprache wirklich so genannt?
Die Syntax stammt zwar von Plan 9 ab, aber wir nennen sie Go-Assembly.
Siehe https://go.dev/doc/asm
Je mehr ich aber gesucht habe, desto mehr wirkt das wie eine LLM-Halluzination.
Die Dokumentation zum Assembly-Format gibt ihm keinen richtigen Namen, sondern nennt es einfach
go assembler.Die Quelle dieser Halluzination ist vermutlich der erste Absatz: „Der Assembler basiert auf dem Eingabestil des Plan-9-Assemblers und … dieses Dokument beschreibt eine Zusammenfassung dieser Syntax, die Unterschiede sowie Besonderheiten beim Schreiben von Assembly-Code, der mit Go interagiert.“
Das ist ungefähr so, als würde man sagen: „Die GNU Compiler Collection verwendet eine eigene interne Assembly-Sprache namens Unix.“
Falls man sich fragt, warum das Go-Team dieses spezielle Assembly-Format gewählt hat: Rob Pike hat 2016 in einem Vortrag das Design des Go-Assemblers behandelt [1][2].
Der Kern war wohl die Beobachtung, dass die meisten Assembly-Sprachen im Großen und Ganzen ähnlich sind, und die Idee, eine gemeinsame Assembly-Sprache zu schaffen, mit der man „mit der untersten Ebene der Maschine sprechen kann, ohne eine neue Syntax lernen zu müssen“.
Außerdem ermöglicht es, automatisch einen funktionierenden Assembler zu erzeugen, indem man das PDF mit dem Befehlsreferenzhandbuch einer neuen Architektur als Eingabe verwendet.
[1]: https://www.youtube.com/watch?v=KINIAgRpkDA
[2]: https://go.dev/talks/2016/asm.slide#1
Es hieß, man habe „Funktionen gebraucht, die SIMD-Operationen auf Slices ausführen“, und ich fragte mich, wie das tatsächlich verwendet wird.
Beim Durchsuchen des gesamten Artikels fand ich nirgends eine Operation, die auf Slices ausgeführt wird.
Update: Ich habe es in der verlinkten Dokumentation gefunden: https://pkg.go.dev/github.com/pehringer/simd#pkg-index
Im Grunde kann man, wenn man zwei Slices addieren möchte, statt einer
for-Schleife SIMD zur parallelen Verarbeitung verwenden, etwasimd.AddInt32(slice1, slice2, result).https://github.com/pehringer/simd/blob/main/simd_test.go
Zu den von Go unterstützten Prozessoren ist dieser Abschnitt relevant (1). Die grundlegende x64-Unterstützung umfasst SSE und SSE2.
Ich weiß allerdings nicht, ob der Go-Compiler das tatsächlich erzeugt. Anders als ein sehr komplexer Compiler wie gcc, der Performance an erste Stelle setzt, bevorzugt der Go-Compiler im Wirth’schen Sinne (2) einen einfachen und schnellen Compiler.
(1) https://go.dev/wiki/MinimumRequirements#amd64
(2) https://irreal.org/blog/?p=7075
https://smartgo.blog/2024/01/06/niklaus-wirth/
Ich glaube, der Autor hat diesen Teil verwechselt; daher ein Referenzlink: https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs
Ich wollte gerade ebenfalls sagen, dass das nach einem Ergebnis einer falschen Interpretation des Codes durch ein LLM aussieht.
Anders kann ich mir kaum vorstellen, dass jemand den Begriff Plan 9 kennt und sich in Assembly hineingräbt, aber nicht weiß, in welches Fahrwasser er da geraten ist. Ich habe dann gesehen, dass andere denselben Gedanken hatten.
Falls das stimmt, hoffe ich, dass der Autor sich nicht schämt oder sich „ertappt“ fühlt, sondern ehrlich damit umgeht. Nur so können wir auch etwas daraus lernen. Ich würde gern mehr Sicherheit bei dieser Art von „LLM-Exposure“ bekommen, aber selbst wenn es noch so offensichtlich wirkt, scheinen es Leute fast nie zuzugeben.
Natürlich ist es hier nicht offensichtlich, sondern nur eine sehr vorschnelle und wertende Vermutung.
So etwas macht mich wirklich wütend.
Das lag ein wenig über meinem Niveau, aber mir hat gefallen, wie der Text die Leser mitgenommen hat.
Das kann doch unmöglich der erste Versuch in diese Richtung sein, oder? Es muss doch buchstäblich Dutzende Gopher geben, die nach SIMD lechzen. Ist das üblichere Muster, CGO zu verwenden?
Eine Assembly-Funktion aus Go aufzurufen ist deutlich günstiger.
Unter https://pkg.go.dev/github.com/grailbio/base/simd gibt es einige Arbeiten von mir in dieser Richtung.
Wenn ich mich richtig erinnere, konnten damals mehrere AVX-Instruktionen vom Plan-9-Assembler von Go nicht einmal kodiert werden, sodass man sie direkt als Bytes kodieren musste [0].
Die ausgereifteste Bibliothek, die ich gesehen habe, habe ich zwar nie benutzt, aber sie verwendet teilweise CGO und nutzt einen sauberen Hack, um dessen Overhead zu vermeiden [1].
[0]: https://github.com/slimsag/rand/blob/f1e8d464c0021a391d5cd64...
[1]: https://github.com/alivanz/go-simd/
Wenn ihr Go-Assembly-Programme schreiben wollt, empfehle ich euch, Avo (https://github.com/mmcloughlin/avo) anzusehen.
Es bietet Typsicherheit und führt einige Prüfungen durch, ob gültiges Assembly ausgegeben wird. Es kann Register dynamisch zuweisen und erspart euch, Dinge wie Stack- und Frame-Größen selbst zu berechnen.
Es kann auch Details der Calling Convention übernehmen, sodass ihr Argumente leicht in die gewünschten Register oder an die gewünschten Positionen laden könnt.
Kürzlich habe ich das gesamte amd64-Assembly in Gos crypto-Bibliothek auf Avo portiert; für solche Arbeiten ist diese Bibliothek sehr nützlich.