HN en clair : mémo sur Go Plan9
(pehringer.info)Exploiter la concurrence et le parallélisme de Go
- Présentation d’un projet visant à améliorer les performances de calcul numérique en exploitant la concurrence et le parallélisme de Go.
- Les instructions SIMD (Same Instruction Multiple Data) permettent d’effectuer des calculs parallèles au niveau matériel.
- Le compilateur de Go n’exploite pas SIMD, et faute d’avoir trouvé un package SIMD généraliste adapté, l’auteur a décidé d’en développer un lui-même.
Langage assembleur Plan9
- Go utilise son propre langage assembleur, appelé Plan9, qui reprend les instructions et registres des plateformes cibles avec quelques adaptations.
- Plan9 pour x86 et Plan9 pour ARM sont différents.
- Les bases de son utilisation sont expliquées à travers un exemple simple de Plan9.
Exemple Plan9
- Les fichiers
AddInts_amd64.setmain.goservent à expliquer les bases de la déclaration et de l’utilisation des fonctions en Plan9. - Le texte décrit comment stocker les arguments de fonction et les valeurs de retour sur la pile selon la convention d’appel de Go.
Plan de conception du package
- Conception d’un package fournissant une fine couche d’abstraction pour les opérations SIMD arithmétiques et bit à bit.
- Création d’un package interne contenant des implémentations Plan9 propres à chaque architecture, configurées via une fonction d’initialisation.
Exemple SIMD
- Un exemple de fonction Plan9 SIMD sur x86 montre comment utiliser SIMD.
- Les fichiers
Supported_amd64.setAddFloat32_amd64.sillustrent comment vérifier la prise en charge de SSE et effectuer une addition sur desfloat32.
Performances et suite
- Un graphique comparant l’implémentation logicielle en Go et l’implémentation SIMD en Plan9 montre un gain de vitesse d’environ 200 à 450 %.
- L’auteur espère que ce mémo inspirera d’autres projets utilisant Plan9 et SIMD.
# Résumé de GN⁺
- Cet article présente une méthode pour maximiser les performances en exploitant la concurrence et le parallélisme de Go.
- Il explique comment effectuer des calculs parallèles au niveau matériel à l’aide du langage assembleur Plan9 et des instructions SIMD.
- Il ouvre aux développeurs Go des pistes d’utilisation de Plan9 et de SIMD, utiles pour explorer de nouvelles approches d’optimisation des performances.
- Parmi les projets comparables, il recommande des bibliothèques de prise en charge SIMD en Rust ou des bibliothèques SIMD liées à C++.
1 commentaires
Avis sur Hacker News
Quelques précisions sur l’assembleur Go : sur amd64, ces
intsont en fait sur 64 bits.Si l’on utilise
int32, la liste des paramètres est alignée sur un mot, mais il y a un piège. Sur les systèmes 64 bits, les valeurs de retour commencent toujours à un décalage aligné sur un double mot.NOSPLITest défini danstextflag.h, fourni automatiquement par le compilateur Go. Mais d’après ce que j’ai lu,NOSPLITne semble respecté que pour les fonctionsruntime.XX, donc ici il ne fait rien, et il n’est pas nécessaire.NOSPLITsignifie que le compilateur ne doit pas insérer le code qui vérifie si la pile risque de déborder et doit être segmentée. Pour une fonction qui n’a pas besoin d’espace de pile, c’est techniquement inutile, et cela existe essentiellement pour éviter que ce code de vérification soit injecté dans la fonction qui vérifie elle-même la segmentation de la pile.Pour ceux qui se demandent pourquoi
4représente « NOSPLIT » et pourquoi il est indiqué comme nécessaire : en général, la taille de frame (le paramètre aprèsNOSPLIT) est suivie de la taille des arguments, les deux étant séparées par un signe moins.Ce n’est pas une soustraction, seulement une syntaxe particulière. Une taille de frame
$24-8signifie que la fonction a une frame de 24 octets et qu’elle est appelée avec 8 octets d’arguments dans la frame de l’appelant.Si
NOSPLITn’est pas spécifié dansTEXT, il faut obligatoirement fournir la taille des arguments. Pour les fonctions en assembleur qui ont un prototype Go,go vetvérifie que la taille des arguments est correcte.Source : https://go.dev/doc/asm
« Go utilise son propre langage assembleur interne appelé Plan9 » : est-ce que ce langage s’appelle vraiment comme ça ?
Sa syntaxe vient bien de Plan 9, mais on l’appelle assembleur Go.
Voir https://go.dev/doc/asm
Mais plus je creuse, plus ça ressemble à une hallucination de LLM.
La documentation du format assembleur ne lui donne pas vraiment de nom ; elle parle simplement de
go assembler.La source de cette hallucination est probablement le premier paragraphe : « L’assembleur est basé sur le style d’entrée de l’assembleur de Plan 9… Le présent document décrit cette syntaxe, ses différences, et les particularités de l’écriture de code assembleur interagissant avec Go. »
C’est un peu comme dire que « GNU Compiler Collection utilise son propre langage assembleur interne appelé Unix ».
Si vous vous demandez pourquoi l’équipe Go a choisi ce format assembleur dédié, Rob Pike a parlé en 2016 de la conception de l’assembleur Go [1][2].
L’idée centrale semblait être que la plupart des langages assembleur se ressemblent globalement, et qu’on pouvait donc créer un langage assembleur commun permettant de « parler au niveau le plus bas de la machine sans avoir à apprendre une nouvelle syntaxe ».
Cela permet aussi de générer automatiquement un assembleur fonctionnel à partir du PDF du manuel d’instructions d’une nouvelle architecture.
[1]: https://www.youtube.com/watch?v=KINIAgRpkDA
[2]: https://go.dev/talks/2016/asm.slide#1
L’article disait qu’il « fallait des fonctions pour effectuer des opérations SIMD sur des slices », et je me demandais comment c’était utilisé en pratique.
En cherchant dans tout l’article, je n’ai trouvé nulle part d’opération effectuée sur des slices.
Modification : je l’ai trouvé dans la documentation liée : https://pkg.go.dev/github.com/pehringer/simd#pkg-index
En gros, si vous avez deux slices que vous voulez additionner, au lieu d’une boucle
for, vous pouvez les traiter en parallèle avec SIMD et écrire quelque chose commesimd.AddInt32(slice1, slice2, result).https://github.com/pehringer/simd/blob/main/simd_test.go
Concernant les processeurs pris en charge par Go, cette section est pertinente (1). Le support x64 de base inclut SSE et SSE2.
Cela dit, je ne sais pas si le compilateur Go les génère réellement. Contrairement à des compilateurs très complexes comme gcc, qui privilégient avant tout les performances, le compilateur Go préfère une approche à la Wirth (2), simple et rapide à compiler.
(1) https://go.dev/wiki/MinimumRequirements#amd64
(2) https://irreal.org/blog/?p=7075
https://smartgo.blog/2024/01/06/niklaus-wirth/
Je pense que l’auteur a confondu cette partie, donc je laisse ce lien de référence : https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs
J’allais justement dire que, moi aussi, ça me donnait l’impression d’être le résultat d’une mauvaise interprétation du code par un LLM
Je vois mal comment expliquer autrement le fait de connaître le terme Plan 9 et d’aller fouiller dans l’assembleur, tout en ne sachant pas vraiment dans quoi on met les pieds. J’ai vu que d’autres avaient eu la même impression
Si c’est bien le cas, j’aimerais que l’auteur soit honnête, sans se sentir honteux ni « démasqué ». C’est comme ça qu’on peut apprendre nous aussi. J’aimerais gagner en certitude sur ce type de « traces de LLM », mais même quand ça paraît évident, les gens semblent très rarement l’admettre
Bien sûr, ici ce n’est pas évident : ce n’est qu’une hypothèse très hâtive et pleine de jugement
Ce genre de chose me met vraiment en colère
Le contenu dépassait un peu mon niveau, mais j’ai apprécié la façon dont l’article emmène le lecteur avec lui
Ça ne peut pas être la première tentative du genre, si ? Il doit littéralement y avoir des dizaines de Gophers assoiffés de SIMD. Le schéma le plus courant consiste-t-il plutôt à utiliser CGO ?
Appeler une fonction assembleur depuis Go est nettement moins coûteux
J’ai des travaux dans cette direction ici : https://pkg.go.dev/github.com/grailbio/base/simd
Si ma mémoire est bonne, à l’époque, plusieurs instructions AVX n’étaient même pas encodables par l’assembleur Plan 9 de Go, et il fallait les encoder directement en octets [0]
La bibliothèque la plus aboutie que j’aie vue, même si je ne l’ai jamais utilisée, emploie une astuce propre pour utiliser partiellement CGO tout en évitant ce surcoût [1]
[0]: https://github.com/slimsag/rand/blob/f1e8d464c0021a391d5cd64...
[1]: https://github.com/alivanz/go-simd/
Pour écrire des programmes en assembleur Go, je recommande de jeter un œil à Avo (https://github.com/mmcloughlin/avo)
Il offre une sûreté de typage et effectue quelques vérifications pour s’assurer qu’il produit de l’assembleur valide. Il peut allouer les registres dynamiquement et évite d’avoir à calculer soi-même des choses comme la pile et la taille de frame
Il peut aussi gérer les détails de la convention d’appel, ce qui permet de charger facilement les arguments dans les registres ou emplacements voulus
J’ai récemment porté vers Avo tout l’assembleur amd64 présent dans la bibliothèque crypto de Go, et c’est une bibliothèque très utile pour ce genre de travail