4 Punkte von GN⁺ 2024-10-19 | 1 Kommentare | Auf WhatsApp teilen

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.s und main.go werden 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.s und AddFloat32_amd64.s wird gezeigt, wie sich SSE-Unterstützung prüfen und eine Additionsoperation für float32 ausfü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

 
GN⁺ 2024-10-19
Hacker-News-Kommentare
  • Ein paar Anmerkungen zu Go-Assembly: Auf amd64 sind diese int tatsächlich 64 Bit groß.
    Wenn man int32 verwendet, wird die Parameterliste zwar wortausgerichtet, aber es gibt einen Haken: Auf 64-Bit-Systemen beginnen Rückgabewerte immer an einem Doubleword-ausgerichteten Offset.
    NOSPLIT ist in textflag.h definiert, das vom Go-Compiler automatisch bereitgestellt wird. Soweit ich es gelesen habe, wird NOSPLIT allerdings wohl nur bei runtime.XX-Funktionen beachtet, tut hier also nichts und ist auch nicht nötig.
    NOSPLIT bedeutet, 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: 4 steht für „NOSPLIT“, und aus irgendeinem Grund heißt es, das sei nötig. Normalerweise folgt nach der Frame-Größe (dem Parameter nach NOSPLIT) die Argumentgröße, getrennt durch ein Minuszeichen.
    Das ist keine Subtraktion, sondern nur eine eigenartige Syntax. Eine Frame-Größe von $24-8 bedeutet, dass die Funktion einen 24-Byte-Frame hat und mit 8 Byte Argumenten im Caller-Frame aufgerufen wird.
    Wenn bei TEXT kein NOSPLIT angegeben ist, muss die Argumentgröße zwingend angegeben werden. Bei Assembly-Funktionen mit Go-Prototyp prüft go 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?

    • Nein. Es ist einfach Go-Assembly.
      Die Syntax stammt zwar von Plan 9 ab, aber wir nennen sie Go-Assembly.
      Siehe https://go.dev/doc/asm
    • Berechtigte Frage. Anfangs dachte ich auch, das stimme einfach. Ich ging davon aus, dass jemand, der dieses Thema recherchiert hat, so etwas nicht falsch machen würde; und wenn man das Projekt ein wenig kennt, wirkt der Name auch irgendwie plausibel.
      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.“
    • Es gibt keinen eigenen Namen dafür. Plan 9 ist ein Betriebssystem, und dieser Assembly-Syntaxstil stammt von dem Assembler ab, der in diesem Betriebssystem verwendet wurde.
      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

    • In der Praxis hat das funktioniert. Go hat die Erwartung geprägt, dass Cross-Compilation für eine neue Programmiersprache eine Grundfähigkeit sein sollte; damals gab es kaum Sprachen, die das gut beherrschten.
    • Nebenbei: SpiderMonkey macht seit etwa 25 Jahren fast dasselbe, und ich nehme an, andere JavaScript-VMs ebenfalls.
  • 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, etwa simd.AddInt32(slice1, slice2, result).

  • 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/

    • In der Praxis unterstützen wohl alle Chips seit etwa 2008 SSE4.1.
  • 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.

    • Ehrlich? Nein. Für die Arroganz, ein LLM als Abkürzung zu betrachten, um eine Arbeit nicht machen zu müssen, die es eigentlich korrekt erledigen sollte, sollte man sich schämen.
      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?

    • Das Problem bei cgo ist der hohe Overhead von Funktionsaufrufen. Man sollte es nur für ziemlich große Arbeitsblöcke einsetzen.
      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.
    • Ich bin mir ziemlich sicher, dass Leute das schon seit Langem versucht haben. Ich erinnere mich an ein Telefonat mit meinem Bruder vor fast zehn Jahren, als er versuchte, eine SIMD-Bibliothek für Go zu bauen (war das bei Skype?).
      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/
    • Der Vorschlag, so etwas in die Standardbibliothek aufzunehmen, wurde abgelehnt, ist aber hilfreich, um den Hintergrund zu verstehen: https://github.com/golang/go/issues/53171
  • 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.