Lorsque des calculs limités en bande passante mémoire sont effectués dans des environnements de mémoire partagée (par exemple, filetés via OpenMP, Pthreads ou TBB), il existe un dilemme sur la façon de garantir que la mémoire est correctement répartie sur la mémoire physique , de sorte que chaque thread accède principalement à la mémoire sur un bus mémoire "local". Bien que les interfaces ne soient pas portables, la plupart des systèmes d'exploitation ont des moyens de définir l'affinité des threads (par exemple pthread_setaffinity_np()
sur de nombreux systèmes POSIX, sched_setaffinity()
Linux, SetThreadAffinityMask()
Windows). Il existe également des bibliothèques telles que hwloc pour déterminer la hiérarchie de la mémoire, mais malheureusement, la plupart des systèmes d'exploitation ne fournissent pas encore de moyens de définir des stratégies de mémoire NUMA. Linux est une exception notable, avec libnumapermettant à l'application de manipuler la politique de mémoire et la migration des pages à la granularité des pages (en ligne principale depuis 2004, donc largement disponible). D'autres systèmes d'exploitation s'attendent à ce que les utilisateurs observent une politique implicite de «premier contact».
Travailler avec une politique de «première touche» signifie que l'appelant doit créer et distribuer des threads avec l'affinité qu'il envisage d'utiliser plus tard lors de la première écriture dans la mémoire fraîchement allouée. (Très peu de systèmes sont configurés de manière à malloc()
trouver réellement des pages, il promet simplement de les trouver lorsqu'elles sont réellement défaillantes, peut-être par des threads différents.) Cela implique que l'allocation utilisant calloc()
ou initialisant immédiatement la mémoire après l'allocation à l'aide memset()
est nuisible car elle aura tendance à défaillir toute la mémoire sur le bus mémoire du cœur exécutant le thread d'allocation, ce qui conduit à la bande passante mémoire la plus défavorable lorsque la mémoire est accessible à partir de plusieurs threads. Il en va de même pour l' new
opérateur C ++ qui insiste pour initialiser de nombreuses nouvelles allocations (par exemplestd::complex
). Quelques observations sur cet environnement:
- L'allocation peut être rendue «collective de threads», mais maintenant l'allocation devient mélangée dans le modèle de thread, ce qui n'est pas souhaitable pour les bibliothèques qui peuvent avoir à interagir avec des clients utilisant différents modèles de thread (peut-être chacun avec leurs propres pools de threads).
- Le RAII est considéré comme une partie importante du C ++ idiomatique, mais il semble nuire activement aux performances de la mémoire dans un environnement NUMA. Le placement
new
peut être utilisé avec de la mémoire allouée viamalloc()
ou des routines delibnuma
, mais cela change le processus d'allocation (ce qui, je pense, est nécessaire). - EDIT: Ma déclaration précédente sur l'opérateur
new
était incorrecte, elle peut prendre en charge plusieurs arguments, voir la réponse de Chetan. Je crois qu'il y a toujours un souci d'obtenir des bibliothèques ou des conteneurs STL pour utiliser l'affinité spécifiée. Plusieurs champs peuvent être compressés et il peut être gênant de s'assurer que, par exemple, unstd::vector
réalloue avec le gestionnaire de contexte correct actif. - Chaque thread peut allouer et fausser sa propre mémoire privée, mais l'indexation dans les régions voisines est plus compliquée. (Considérons un produit matriciel-vecteur clairsemé avec une partition en ligne de la matrice et des vecteurs; l'indexation de la partie non possédée de x nécessite une structure de données plus compliquée lorsque x n'est pas contigu dans la mémoire virtuelle.)
Est-ce que des solutions à l'allocation / initialisation NUMA sont considérées comme idiomatiques? Ai-je omis d'autres problèmes critiques?
(Je ne veux pas pour mon C ++ exemples pour impliquer l'accent sur cette langue, mais le C ++ langage code des décisions sur la gestion de la mémoire qu'une langue comme C n'a pas, donc il a tendance à être une plus grande résistance en suggérant que les programmeurs C ++ font les les choses différemment.)
la source
Cette réponse est en réponse à deux idées fausses liées à C ++ dans la question.
Ce n'est pas une réponse directe aux problèmes multicœurs que vous mentionnez. Il suffit de répondre aux commentaires qui classent les programmeurs C ++ en tant que fanatiques C ++ afin que la réputation soit maintenue;).
Point 1. C ++ "new" ou allocation de pile n'insiste pas sur l'initialisation de nouveaux objets, qu'ils soient POD ou non. Le constructeur par défaut de la classe, tel que défini par l'utilisateur, a cette responsabilité. Le premier code ci-dessous montre les déchets imprimés, que la classe soit POD ou non.
Au point 2. C ++ permet de surcharger "new" avec plusieurs arguments. Le deuxième code ci-dessous montre un tel cas pour l'allocation d'objets uniques. Cela devrait donner une idée et peut-être être utile pour la situation que vous avez. L'opérateur new [] peut également être modifié de manière appropriée.
// Code pour le point 1.
Le compilateur Intel 11.1 affiche cette sortie (qui est bien sûr de la mémoire non initialisée pointée par "a").
// Code pour le point 2.
la source
std::complex
qui sont explicitement initialisés.std::complex
?Nous avons l'infrastructure logicielle pour paralléliser l'assemblage sur chaque cellule sur plusieurs cœurs en utilisant les blocs de construction de thread (en gros, vous avez une tâche par cellule et devez planifier ces tâches sur les processeurs disponibles - ce n'est pas comme ça que c'est mais c'est l'idée générale). Le problème est que pour l'intégration locale, vous avez besoin d'un certain nombre d'objets temporaires (scratch) et vous devez en fournir au moins autant qu'il y a de tâches pouvant s'exécuter en parallèle. Nous constatons une mauvaise accélération, probablement parce que lorsqu'une tâche est placée sur un processeur, elle récupère l'un des objets de travail qui se trouvent généralement dans le cache d'un autre cœur. Nous avions deux questions:
(i) Est-ce vraiment la raison? Lorsque nous exécutons le programme sous cachegrind, je vois que j'utilise essentiellement le même nombre d'instructions que lors de l'exécution du programme sur un seul thread, mais la durée d'exécution totale accumulée sur tous les threads est beaucoup plus grande que celle à un seul thread. Est-ce vraiment parce que je blâme continuellement le cache?
(ii) Comment puis-je savoir où je suis, où se trouvent chacun des objets de travail et quel objet de travail je devrais prendre pour accéder à celui qui est chaud dans le cache de mon noyau actuel?
En fin de compte, nous n'avons trouvé de réponse à aucune de ces solutions et après quelques travaux, nous avons décidé que nous manquions d'outils pour enquêter et résoudre ces problèmes. Je sais comment au moins en principe résoudre le problème (ii) (à savoir, en utilisant des objets thread-local, en supposant que les threads restent épinglés aux cœurs du processeur - une autre conjecture qui n'est pas triviale à tester), mais je n'ai pas d'outils pour tester le problème (je).
Donc, de notre point de vue, traiter avec NUMA est toujours une question non résolue.
la source
Au-delà de hwloc, il existe quelques outils qui peuvent générer des rapports sur l'environnement de mémoire d'un cluster HPC et qui peuvent être utilisés pour définir une variété de configurations NUMA.
Je recommanderais LIKWID comme un tel outil car il évite une approche basée sur le code vous permettant par exemple d'épingler un processus à un noyau. Cette approche de l'outillage pour traiter la configuration de la mémoire spécifique à la machine contribuera à garantir la portabilité de votre code entre les clusters.
Vous pouvez trouver une brève présentation de ISC'13 " LIKWID - Lightweight Performance Tools " et les auteurs ont publié un article sur Arxiv " Meilleures pratiques pour l'ingénierie de performance assistée par HPM sur un processeur multicœur moderne ". Cet article décrit une approche pour interpréter les données des compteurs matériels afin de développer un code performant spécifique à l'architecture et à la topologie de la mémoire de votre machine.
la source