4 points par GN⁺ 2024-10-19 | 1 commentaires | Partager sur WhatsApp

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.s et main.go servent à 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.s et AddFloat32_amd64.s illustrent comment vérifier la prise en charge de SSE et effectuer une addition sur des float32.

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

 
GN⁺ 2024-10-19
Avis sur Hacker News
  • Quelques précisions sur l’assembleur Go : sur amd64, ces int sont 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.
    NOSPLIT est défini dans textflag.h, fourni automatiquement par le compilateur Go. Mais d’après ce que j’ai lu, NOSPLIT ne semble respecté que pour les fonctions runtime.XX, donc ici il ne fait rien, et il n’est pas nécessaire.
    NOSPLIT signifie 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 4 représente « NOSPLIT » et pourquoi il est indiqué comme nécessaire : en général, la taille de frame (le paramètre après NOSPLIT) 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-8 signifie 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 NOSPLIT n’est pas spécifié dans TEXT, il faut obligatoirement fournir la taille des arguments. Pour les fonctions en assembleur qui ont un prototype Go, go vet vé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 ?

    • Non. C’est simplement de l’assembleur Go.
      Sa syntaxe vient bien de Plan 9, mais on l’appelle assembleur Go.
      Voir https://go.dev/doc/asm
    • C’est une question légitime. Au début, j’ai aussi supposé que c’était correct. Je me disais que quelqu’un qui avait étudié le sujet n’aurait pas pu se tromper là-dessus, et que, quand on connaît un peu le projet, ce nom paraît relativement plausible.
      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. »
    • Il n’a pas de nom distinct. Plan 9 est un système d’exploitation, et le style de syntaxe de cet assembleur vient de l’assembleur utilisé dans ce système.
      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

    • En pratique, ça a marché. Go a créé l’attente qu’un nouveau langage de programmation doive proposer la compilation croisée comme capacité de base, alors qu’à l’époque très peu de langages le faisaient bien.
    • À titre de référence, SpiderMonkey fait à peu près la même chose depuis environ 25 ans, et j’imagine que les autres machines virtuelles JavaScript aussi.
  • 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 comme simd.AddInt32(slice1, slice2, result).

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

    • En réalité, je pense que toutes les puces depuis environ 2008 prennent en charge SSE4.1.
  • 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

    • Franchement ? Non. Il devrait avoir honte de cette arrogance consistant à penser qu’un LLM est un raccourci dispensant de faire le travail qu’il est censé savoir faire correctement
      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 ?

    • Le problème de cgo, c’est le coût élevé d’un appel de fonction. Il vaut mieux ne l’utiliser que pour des blocs de travail assez conséquents
      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
    • Je suis sûr que des gens essaient ça depuis longtemps. Je me souviens d’un appel avec mon frère il y a presque dix ans, quand il essayait de créer une bibliothèque SIMD en Go (chez Skype, peut-être ?)
      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/
    • La proposition d’ajouter ce genre de chose à la bibliothèque standard a été rejetée, mais elle aide à comprendre le contexte : https://github.com/golang/go/issues/53171
  • 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