Cette page va décrire comment utiliser la mémoire partagée entre les processus d'un même noeud de calcul à l'aide de fenêtres mémoires.
Une fenêtre mémoire est un espace mémoire alloué dans une partie de la mémoire accessible par tous les processus. Cette fenêtre sera découpée en plusieurs segments, un par processus.
Deux implémentations différentes sont disponibles : une avec une taille constante, définie lors de la construction de l'objet et une autre avec une taille pouvant évoluer.
Cette implémentation va permettre de créer une fenêtre mémoire ayant tous ses segments contigüs. Il est ainsi assez simple de redécouper les segments pendant l'utilisation (par exemple, pour équilibrer un calcul).
Cette partie est gérée par la classe Arcane::MachineMemoryWindow.
Cette classe peut utiliser trois implémentations de IMachineMemoryWindowBase, une par type de Arcane::IParallelMng. Il est donc possible d'utiliser cette classe de la même façon, que l'on ait un MpiParallelMng, un SequentialParallelMng, un SharedMemoryParallelMng ou un HybridParallelMng (Choix du gestionnaire d'échange de message).
La création d'un objet de ce type est collectif. Une instance de cette classe va créer une fenêtre mémoire composée de plusieurs segments (un par sous-domaine).
L'accès aux éléments des segments n'est pas collectif. L'accès concurrent à un élément est possible en utilisant des sémaphores, des mutex ou des std::atomic. Pour les std::atomic, il faut que les opérations soient address-free
:
Lors de la construction de cet objet, chaque sous-domaine va fournir une taille de segment. La taille de la fenêtre va être égale à la somme des tailles de segments.
Pour accéder à son segment, il est possible d'utiliser la méthode Arcane::MachineMemoryWindow::segmentView().
Une fois le segment modifié, on peut faire une barrier pour s'assurer que tout le monde a écrit dans son segment avant de s'en servir.
Pour savoir quels sous-domaines se partagent une fenêtre sur le noeud, il est possible de récupérer un tableau de rang.
La position des rangs dans ce tableau correspond à la position de leur segment dans la fenêtre.
Pour la lecture des segments des autres sous-domaines du noeud, on peut utiliser la méthode Arcane::MachineMemoryWindow::segmentConstView().
La taille de la fenêtre ne peut pas être modifiée. En revanche, l'implémentation dans Arcane permet de redimensionner les segments de manière collectif (à la condition que la nouvelle taille de la fenêtre soit inférieure ou égale à la taille d'origine).
La fenêtre étant contigüe, l'accès à toute la fenêtre est possible pour tous les sous-domaines.
Cette implémentation est assez différente de la précédente. Ici, les segments des fenêtres mémoires ne sont plus contigüs. Cela nous permet de redimensionner les segments comme un tableau dynamique classique.
Néanmoins, cette opération est collective, ce qui contamine la plupart des méthodes de l'implémentation.
Cette partie est gérée par la classe Arcane::DynamicMachineMemoryWindow.
Comme pour la précédente implémentation, celle-ci est compatible avec tous les modes de parallélisme de Arcane.
La création d'un objet de ce type est collectif. Une instance de cette classe va créer une fenêtre mémoire composée de plusieurs segments (un par sous-domaine).
Comme un UniqueArray, il est possible de spécifier une taille initiale (ici 5
) :
Et il est possible de ne pas spécifier de taille initiale.
La méthode Arcane::DynamicMachineMemoryWindow::machineRanks() est disponible et renvoie le même tableau que l'implémentation Arcane::MachineMemoryWindow.
Pour explorer notre segment ou le segment d'un autre sous-domaine, il est possible d'utiliser les mêmes méthodes que précédemment :
En revanche, comme les segments ne sont pas contigüs, les méthodes windowView()
ne sont pas disponibles.
Les segments ont une taille qui peut être augmentée ou diminuée au cours du temps.
Il est possible d'ajouter des éléments avec la méthode Arcane::DynamicMachineMemoryWindow::add(Arcane::Span<const Type> elem) :
Cette méthode est collective, tous les sous-domaines d'un noeud doivent l'appeler. Si un sous-domaine ne souhaite pas ajouter d'éléments dans son segment, il peut appeler la méthode add()
avec un tableau vide ou sans argument (Arcane::DynamicMachineMemoryWindow::add()).
Cette opération peut être couteuse à cause de la réallocation mémoire. Il est donc conseillé d'ajouter une grande quantité d'éléments en une fois plutôt qu'élément par élément.
Si l'ajout élément par élément est indispensable, la méthode Arcane::DynamicMachineMemoryWindow::reserve(Arcane::Int64 new_capacity) est disponible afin d'éviter de réallouer plusieurs fois un segment :
Cette méthode va réserver un espace de 20
Integer
pour tous les sous-domaines (cette valeur peut être différente pour chaque sous-domaine) (si un sous-domaine ne veut pas réserver plus d'espace, il peut appeler Arcane::DynamicMachineMemoryWindow::reserve()).
reserve(0)
n'a aucun effet). Pour réduire l'espace réservé, la méthode Arcane::DynamicMachineMemoryWindow::shrink() est disponible.add()
ou sans resize()
. La seconde change le nombre d'éléments du segment et appelle reserve()
si nécessaire.Ce resize()
va augmenter le nombre d'éléments de tous les segments sauf du sous-domaine qui avait fait les add()
. Ce sous-domaine va passer de 15 éléments à 12 (comme pour reserve()
, chaque sous-domaine peut mettre la valeur qu'il veut).
Il est aussi possible d'ajouter des éléments dans le segment d'un autre sous-domaine avec la méthode Arcane::DynamicMachineMemoryWindow::addToAnotherSegment(Arcane::Int32 rank, Arcane::Span<const Type> elem).
add()
et à addToAnotherSegment()
. Si un sous-domaine appelle la méthode addToAnotherSegment()
, tous les sous-domaines devront appeler collectivement addToAnotherSegment()
(avec ou sans paramètres) et pas add()
.Le fonctionnement est presque identique à la méthode add()
mais avec un paramètre en plus pour désigner le rang du sous-domaine possédant le segment à modifier.
La mémoire partagée entre processus ne doit pas être vu comme la mémoire partagée en multithread. Ce partage n'est fait que sur une partie de la mémoire, pas sur toute la mémoire.
Prenons cette structure :
On peut l'utiliser comme ceci :
Si on utilise cette structure dans une fenêtre, ça donnerait :
On peut afficher la valeur que l'on a attribuée, ça fonctionne correctement :
Mais si on veut afficher la valeur d'un autre processus :
En multi-processus (lancement avec mpirun -n 2 ...
), le programme va planter (segfault), alors qu'il ne plantera pas en multithreading (lancement avec -A,S=2
).
En multi-processus, les attributs du tableau UniqueArray<Integer> array_integer;
de la structure ne sont pas alloués en mémoire partagée (les new
ou malloc
sont faits sur la mémoire locale), les autres processus n'y ont donc pas accès.
Il est aussi important de noter qu'un même emplacement mémoire en mémoire partagée est adressé différemment entre les processus. Donc, si l'on donne un allocateur en mémoire partagée à l'UniqueArray
, les adresses utilisées seront valables uniquement localement.