Cela m’amène à me demander quelle est l’importance du multithreading dans le scénario actuel du secteur.
Dans les domaines où les performances sont critiques et où les performances ne proviennent pas de codes tiers qui effectuent des tâches lourdes, mais des nôtres, j'aurais alors tendance à considérer les choses dans cet ordre d'importance du point de vue de la CPU (le GPU est un joker que j'ai gagné). n'entre pas dans):
- Efficacité de la mémoire (ex: localité de référence).
- Algorithmique
- Multithreading
- SIMD
- Autres optimisations (indications de prédiction de branche statique, par exemple)
Notez que cette liste n’est pas uniquement basée sur l’importance mais bien sur d’autres dynamiques telles que leur impact sur la maintenance, leur simplicité (si elles méritent d’être considérées plus à l’avance), leurs interactions avec d’autres sur la liste, etc.
Efficacité de la mémoire
La plupart pourraient être surpris de mon choix d'efficacité de la mémoire sur l'algorithmique. C'est parce que l'efficacité de la mémoire interagit avec les 4 autres éléments de cette liste, et parce que son examen est souvent beaucoup dans la catégorie "conception" plutôt que dans la catégorie "implémentation". Il y a certes un problème d'œuf ou de poule ici car comprendre l'efficacité de la mémoire nécessite souvent de prendre en compte les 4 éléments de la liste, tandis que les 4 autres éléments nécessitent également de prendre en compte l'efficacité de la mémoire. Pourtant, c'est au cœur de tout.
Par exemple, si nous avons besoin d’une structure de données offrant un accès séquentiel linéaire et des insertions constantes à l’arrière et rien d’autre pour les petits éléments, le choix naïf à rechercher ici serait une liste chaînée. Cela ne tient pas compte de l'efficacité de la mémoire. Lorsque nous considérons l'efficacité de la mémoire dans le mélange, nous finissons par choisir des structures plus contiguës dans ce scénario, telles que des structures développables basées sur des tableaux ou des nœuds plus contigus (par exemple, un stockant 128 éléments dans un nœud) liés entre eux, ou à tout le moins. une liste chaînée soutenue par un allocateur de pool. Celles-ci ont un avantage spectaculaire en dépit de la même complexité algorithmique. De même, nous choisissons souvent le tri rapide d'un tableau sur le tri par fusion malgré une complexité algorithmique inférieure simplement en raison de l'efficacité de la mémoire.
De même, nous ne pouvons pas avoir un multithreading efficace si nos modèles d'accès à la mémoire sont tellement granulaires et dispersés dans la nature que nous finissons par maximiser la quantité de faux partage lors du verrouillage aux niveaux les plus granulaires du code. L'efficacité de la mémoire multiplie donc l'efficacité du multithreading. C'est une condition préalable pour tirer le meilleur parti des threads.
Chaque élément situé au-dessus de la liste présente une interaction complexe avec les données, et le fait de se concentrer sur la façon dont les données sont représentées dépend en définitive de l'efficacité de la mémoire. Chacune de ces solutions peut être gênée par un moyen inapproprié de représenter ou d’accéder aux données.
Une autre efficacité de la mémoire de la raison est si important est qu'il peut appliquer tout au long de l' ensemble du code de base. Généralement, lorsque les gens imaginent que des inefficacités s’accumulent dans de petites parties du travail ici et là, c’est le signe qu’ils ont besoin d’un profileur. Pourtant, les champs à faible temps de latence ou ceux qui utilisent un matériel très limité trouveront, même après le profilage, des sessions n’indiquant pas de points chauds clairs (dispersés dans l’espace) dans une base de code qui est manifestement inefficace en termes d’allocation, de copie et de copie. accéder à la mémoire. En règle générale, il s'agit du seul moment où une base de code entière peut être affectée par des problèmes de performances pouvant conduire à un nouvel ensemble de normes appliquées dans toute la base de code, l'efficacité de la mémoire étant souvent au cœur de celle-ci.
Algorithmique
Celui-ci est à peu près une donnée, car le choix d'un algorithme de tri peut faire la différence entre une entrée massive prenant des mois à trier et des secondes à trier. L’impact le plus important réside dans le choix entre des algorithmes quadratiques ou cubiques vraiment sous-pairs et un algorithme linéarithmique, ou entre des algorithmes linéaires et logarithmiques ou constants, du moins jusqu’à ce que nous ayons environ 1 000 000 machines de base (auquel cas la mémoire). l'efficacité deviendrait encore plus importante).
Cependant, cela ne figure pas en haut de ma liste personnelle, car toute personne compétente dans son domaine saurait utiliser une structure d’accélération pour l’abattage frustum, par exemple: un arbre de base pour les recherches basées sur le préfixe est une affaire de bébé. En l'absence de ce type de connaissances de base du domaine dans lequel nous travaillons, l'efficacité algorithmique pourrait certainement atteindre le sommet, mais souvent, l'efficacité algorithmique est triviale.
Inventer de nouveaux algorithmes peut également s'avérer une nécessité dans certains domaines (par exemple, dans le traitement des maillages, j'ai dû inventer des centaines, soit qu'ils n'existaient pas auparavant, soit que les implémentations de fonctionnalités similaires dans d'autres produits étaient des secrets propriétaires, non publiés dans un article. ). Cependant, une fois que nous avons dépassé la partie consacrée à la résolution de problèmes et que nous avons trouvé le moyen d'obtenir les bons résultats, une fois que l'efficacité est devenue l'objectif recherché, le seul moyen de gagner réellement est de considérer la manière dont nous interagissons avec les données (mémoire). Sans comprendre l'efficacité de la mémoire, le nouvel algorithme peut devenir inutilement complexe avec de vains efforts pour le rendre plus rapide, alors que la seule chose dont il avait besoin était de prendre un peu plus en compte l'efficacité de la mémoire pour obtenir un algorithme plus simple et plus élégant.
Enfin, les algorithmes ont tendance à appartenir davantage à la catégorie "implémentation" qu'à l'efficacité de la mémoire. Ils sont souvent plus faciles à améliorer avec le recul, même avec un algorithme sous-optimal utilisé au départ. Par exemple, un algorithme de traitement d'image inférieur est souvent simplement implémenté à un emplacement local dans la base de code. Il peut être échangé contre un meilleur plus tard. Cependant, si tous les algorithmes de traitement d'image sont liés à une Pixel
interface ayant une représentation de mémoire sous-optimale, mais que le seul moyen de la corriger est de changer la façon dont plusieurs pixels sont représentés (et non un seul), nous sommes souvent SOL et devra réécrire complètement la base de code vers unImage
interface. Il en va de même pour le remplacement d'un algorithme de tri - il s'agit généralement d'un détail d'implémentation, tandis qu'une modification complète de la représentation sous-jacente des données en cours de tri ou de la manière dont elles sont transmises dans les messages peut nécessiter une nouvelle conception des interfaces.
Multithreading
Le multithreading est une tâche difficile du point de vue des performances car il s’agit d’une optimisation au niveau micro-informatique qui tient compte des caractéristiques matérielles, mais notre matériel évolue réellement dans cette direction. J'ai déjà des pairs qui ont 32 cœurs (j'en ai seulement 4).
Pourtant, la lecture à plusieurs pistes compte parmi les micro-optimisations les plus dangereuses probablement connues d'un professionnel si le but est utilisé pour accélérer les logiciels. La situation de concurrence critique est probablement le bogue le plus mortel possible, car elle est de nature tellement indéterministe (peut-être qu’une seule apparition tous les deux ou trois mois sur la machine d’un développeur à un moment des plus inconfortables en dehors d’un contexte de débogage, le cas échéant). On peut donc affirmer que la dégradation la plus négative en termes de maintenabilité et d’exactitude potentielle du code figure parmi celles-ci, d’autant plus que les bogues liés au multithreading peuvent facilement passer inaperçu lors des tests les plus minutieux.
Néanmoins, cela devient si important. Bien que le nombre de cœurs que nous avons actuellement ne soit pas toujours aussi efficace que l’efficacité de la mémoire (ce qui peut parfois rendre les choses cent fois plus rapide), nous en voyons de plus en plus. Bien sûr, même avec des machines à 100 cœurs, je mettrais toujours l’efficacité de la mémoire en tête de liste, car l’efficacité des threads est généralement impossible sans elle. Un programme peut utiliser une centaine de threads sur une telle machine tout en restant lent, sans une représentation efficace de la mémoire et des modèles d'accès (qui sont liés aux modèles de verrouillage).
SIMD
SIMD est également un peu gênant puisque les registres s’élargissent, avec des plans pour l’élargir. À l'origine, nous avons vu des registres MMX 64 bits suivis de registres XMM 128 bits capables de 4 opérations SPFP en parallèle. Nous voyons maintenant des registres YMM 256 bits capables de 8 en parallèle. Et il y a déjà des plans en place pour des registres de 512 bits qui permettraient 16 en parallèle.
Ceux-ci interagiraient et se multiplieraient avec l'efficacité du multithreading. Cependant, SIMD peut dégrader la maintenabilité tout autant que le multithreading. Même si les bogues qui leur sont associés ne sont pas nécessairement aussi difficiles à reproduire et à corriger qu'un blocage ou une situation critique, la portabilité est délicate, et il est essentiel de s'assurer que le code peut être exécuté sur la machine de tout le monde (et en utilisant les instructions appropriées en fonction de leurs capacités matérielles). gênant.
Une autre chose est que bien que les compilateurs ne battent généralement pas le code SIMD écrit avec brio, ils battent facilement les tentatives naïves. Ils pourraient s’améliorer à un point tel que nous n’avons plus à le faire manuellement, ou du moins que nous ne devenions pas assez manuels pour écrire des codes intrinsèques ou des assemblages simples (peut-être juste un peu de guidage humain).
Encore une fois cependant, sans une disposition de mémoire efficace pour le traitement vectorisé, SIMD est inutile. Nous finirons par charger un seul champ scalaire dans un registre large uniquement pour y effectuer une opération. Au cœur de tous ces éléments, il y a une dépendance à la disposition de la mémoire pour être vraiment efficace.
Autres optimisations
Celles-ci sont souvent ce que je suggérerais de commencer à appeler «micro» de nos jours si le mot suggère non seulement d’aller au-delà de l’algorithme, mais également d’apporter des modifications qui ont un impact minime sur les performances.
Tenter d'optimiser la prédiction de branche nécessite souvent une modification de l'algorithme ou de l'efficacité de la mémoire, par exemple, si cela n'est tenté que par des astuces et un réarrangement du code pour la prédiction statique, cela ne sert qu'à améliorer la première exécution de ce code, ce qui rend les effets douteux. pas souvent carrément négligeable.
Retour à Multithreading pour la performance
Quoi qu'il en soit, quelle est l'importance du multithreading dans un contexte de performance? Sur ma machine à 4 cœurs, elle peut idéalement être 5 fois plus rapide (ce que je peux obtenir avec l'hyperthreading). Ce serait beaucoup plus important pour mon collègue qui a 32 cœurs. Et cela deviendra de plus en plus important dans les années à venir.
C'est donc très important. Mais il est inutile de simplement lancer un tas de discussions sur le problème si l'efficacité de la mémoire n'est pas là pour permettre l'utilisation de verrous avec parcimonie, pour réduire le faux partage, etc.
Multithreading en dehors de la performance
Le multithreading ne concerne pas toujours la simple performance au sens de débit simple. Parfois, il est utilisé pour équilibrer une charge, même au coût possible du débit, afin d'améliorer la réactivité de l'utilisateur, ou pour permettre à l'utilisateur de faire plus de tâches multiples sans attendre la fin de l'opération (ex: continuer à naviguer pendant le téléchargement d'un fichier).
Dans ces cas, je suggérerais que le multithreading monte encore plus haut (peut-être même au-dessus de l'efficacité de la mémoire), puisqu'il s'agit alors d'une conception utilisateur plutôt que de tirer le meilleur parti du matériel. Cela va souvent dominer les conceptions d'interface et la façon dont nous structurons l'ensemble de notre base de code dans de tels scénarios.
Lorsque nous ne parallélisons pas simplement une boucle étroite en accédant à une structure de données volumineuse, le multithreading passe dans la catégorie des "concepteurs" vraiment extrêmes, et le design l'emporte toujours sur la mise en oeuvre.
Donc, dans ces cas, je dirais que considérer le multithreading dès le départ est absolument essentiel, même plus que la représentation en mémoire et l’accès.