1 points par GN⁺ 2024-01-06 | 1 commentaires | Partager sur WhatsApp

Accélération du code : sur AMD64, ne transmettez pas de structures de plus de 16 octets

  • Pour améliorer les performances du langage Neat, la méthode de transmission a été modifiée : au lieu de passer un tableau comme un seul paramètre de structure, il est désormais transmis sous forme de trois paramètres pointeurs.
  • La raison pour laquelle les tableaux de Neat étaient plus lents que ceux du langage D est qu’un tableau de 24 octets dépasse 16 octets, ce qui entraîne un mode de passage des paramètres différent.
  • Selon la spécification de l’ABI SystemV AMD64, toutes les structures de plus de 16 octets sont transmises via un pointeur.

Vérification du problème par benchmark

  • Le benchmark a permis de confirmer la différence de performances entre le passage d’une structure et le passage de champs individuels.
  • Lorsqu’on transmet une structure, il faut l’allouer sur la pile puis la copier, alors que des champs individuels peuvent être transmis directement via les registres SSE.
  • La transmission par champs individuels offre des performances environ 2 fois supérieures à celle du passage d’une structure.

Le choix du concepteur de langage

  • Lorsqu’on appelle une API C, il faut respecter l’ABI C, mais les types de haut niveau utilisés en interne n’ont pas nécessairement besoin d’être représentés comme des structures.
  • Le concepteur du langage peut décider comment seront transmis les tableaux, tuples et types somme.
  • Transmettre séparément les champs des types dépassant 16 octets peut contribuer à améliorer les performances.

L’avis de GN⁺

  • Cet article est très utile pour les développeurs qui s’intéressent à l’optimisation logicielle.
  • Il montre notamment que, lors du développement d’applications sensibles aux performances, la taille des structures et leur mode de transmission peuvent avoir un impact important.
  • Les concepteurs de langages et les développeurs d’API peuvent exploiter ces informations pour identifier des opportunités d’amélioration des performances.

1 commentaires

 
GN⁺ 2024-01-06
Avis Hacker News
  • En lien avec le problème de l’ABI SysV amd64, il est possible de définir l’ABI interne du langage sur autre chose que SysV. Tant qu’elle n’est pas exposée à des appelants C SysV, on peut utiliser la convention d’appel souhaitée. La différence avec NeatLang semble bien plus complexe qu’un simple changement de convention d’appel LLVM, et l’auteur peut aussi vouloir exposer des types aux programmes C avec une convention d’appel cohérente.
  • Beaucoup de gens comprennent mal le coût du passage des arguments, et l’article écrit à ce sujet est instructif. Par exemple, chez Google, la pratique consistant à passer des objets de 24 octets par valeur n’apparaît pas dans le profiler, mais elle a pourtant un coût dans chaque fonction.
  • Lors du passage à x64, un moteur graphique a été benchmarké par crainte de voir les objets vec3 (3xfloat) passer de 12 à 16 octets. Il s’est avéré que l’utilisation de 16 octets était plus rapide, car mieux alignée sur les lectures de 8 octets. Au final, vec3 a été utilisé comme vec4. Il est recommandé de toujours effectuer des benchmarks globaux.
  • Des arguments préchargés dans les registres sont plus performants qu’une écriture sur la pile, et la manipulation de la pile est plus rapide qu’une allocation sur le tas. C’est pourquoi du code complexe avec beaucoup de variables globales peut s’exécuter rapidement, alors que des fonctions récursives élégantes ou des arguments de type tuple/struct/liste sont lents. Le premier cas est plus facile à optimiser en boucles assembleur denses.
  • Avec MSVC, les structures de plus de 8 octets sont passées sur la pile. C’est un détail d’ABI sur lequel il ne faut pas compter dans du code portable. En revanche, pour les fonctions peu appelées, inutile de trop s’en inquiéter, et pour les petites fonctions fréquemment appelées, il vaut mieux permettre au compilateur d’inliner le code, ce qui active des optimisations plus utiles que le simple passage des arguments par registres.
  • Sous Windows, lorsque la convention d’appel cdecl par défaut est utilisée, les structures de plus de 8 octets ne sont pas passées dans les registres.
  • Sur amd64, avec l’ABI sysv amd64, passer et retourner par valeur des structures de plus de 16 octets est lent, mais cela vaut souvent le coup pour rendre le code plus clair. Bien sûr, ce n’est pas le cas ici, mais on peut par exemple utiliser une ABI personnalisée à l’intérieur de son propre langage, comme le font chaque compilateur C++, Golang, OCaml ou SBCL.
  • En C++, il existe une règle empirique selon laquelle les types non primitifs doivent être passés par référence (ou par pointeur si nécessaire), sauf bonne raison de faire autrement. C’est en partie à cause de l’ABI, et aussi pour éviter les constructeurs de copie ou de déplacement. Si l’on veut optimiser les performances, ce sont des détails bas niveau ennuyeux auxquels il faut faire attention en C++.
  • L’article renvoie vers un benchmark très spécifique dans lequel Java (JIT) est plus rapide que C++, et même plus rapide que Scala. Cela soulève des questions sur ce qu’est Julia HO et pourquoi c’est si rapide, sur l’écart de vitesse important entre Python et Pypy, et sur les raisons éventuelles de ne pas utiliser Pypy ou de ne pas en faire le standard.
  • Dans l’exemple fourni, on peut corriger le problème sans affecter l’appelant en changeant le type de paramètre struct Vector pour le passer par référence const struct Vector &. Beaucoup de code C++ sujet à des bugs de pointeur utilisait des pointeurs inutilement, alors qu’un passage par référence aurait été plus simple et plus sûr.