Partager via


NUMA Architecture

Le modèle traditionnel pour l’architecture multiprocesseur est un multiprocesseur symétrique (SMP). Dans ce modèle, chaque processeur a un accès égal à la mémoire et aux E/S. À mesure que d’autres processeurs sont ajoutés, le bus du processeur devient une limitation pour les performances du système.

Les concepteurs système utilisent l’accès à la mémoire non uniforme (NUMA) pour augmenter la vitesse du processeur sans augmenter la charge sur le bus du processeur. L’architecture n’est pas uniforme, car chaque processeur est proche de certaines parties de la mémoire et loin d’autres parties de la mémoire. Le processeur obtient rapidement l’accès à la mémoire à laquelle il est proche, tandis qu’il peut prendre plus de temps pour accéder à la mémoire plus éloignée.

Dans un système NUMA, les processeurs sont organisés dans des systèmes plus petits appelés nœuds. Chaque nœud a ses propres processeurs et mémoire, et est connecté au système plus grand par le biais d’un bus d’interconnexion cohérent avec le cache.

Le système tente d’améliorer les performances en planifiant des threads sur des processeurs qui se trouvent dans le même nœud que la mémoire utilisée. Il tente de satisfaire les demandes d’allocation de mémoire à partir du nœud, mais alloue de la mémoire à partir d’autres nœuds si nécessaire. Il fournit également une API pour rendre la topologie du système disponible pour les applications. Vous pouvez améliorer les performances de vos applications à l’aide des fonctions NUMA pour optimiser la planification et l’utilisation de la mémoire.

Tout d’abord, vous devez déterminer la disposition des nœuds dans le système. Pour récupérer le nœud numéroté le plus élevé dans le système, utilisez la fonction GetNumaHighestNodeNumber. Notez que ce nombre n’est pas garanti pour être égal au nombre total de nœuds dans le système. En outre, les nœuds avec des nombres séquentiels ne sont pas garantis pour être proches. Pour récupérer la liste des processeurs sur le système, utilisez la fonction GetProcessAffinityMask. Vous pouvez déterminer le nœud de chaque processeur de la liste à l’aide de la fonction GetNumaProcessorNode. Vous pouvez également récupérer une liste de tous les processeurs d’un nœud, utiliser la fonction GetNumaNodeProcessorMask.

Une fois que vous avez déterminé quels processeurs appartiennent aux nœuds, vous pouvez optimiser les performances de votre application. Pour vous assurer que tous les threads de votre processus s’exécutent sur le même nœud, utilisez la fonction SetProcessAffinityMask avec un masque d’affinité de processus qui spécifie les processeurs dans le même nœud. Cela augmente l’efficacité des applications dont les threads doivent accéder à la même mémoire. Sinon, pour limiter le nombre de threads sur chaque nœud, utilisez la fonction SetThreadAffinityMask.

Les applications nécessitant beaucoup de mémoire devront optimiser leur utilisation de la mémoire. Pour récupérer la quantité de mémoire libre disponible pour un nœud, utilisez la fonction GetNumaAvailableMemoryNode. La fonction VirtualAllocExNuma permet à l’application de spécifier un nœud préféré pour l’allocation de mémoire. VirtualAllocExNuma n’alloue pas de pages physiques. Il réussit donc si les pages sont disponibles sur ce nœud ou ailleurs dans le système. Les pages physiques sont allouées à la demande. Si le nœud préféré manque de pages, le gestionnaire de mémoire utilise des pages d’autres nœuds. Si la mémoire est paginée, le même processus est utilisé lorsqu’il est renvoyé.

Prise en charge de NUMA sur les systèmes avec plus de 64 processeurs logiques

Sur les systèmes avec plus de 64 processeurs logiques, les nœuds sont affectés à groupes de processeurs en fonction de la capacité des nœuds. La capacité d’un nœud est le nombre de processeurs présents lorsque le système démarre avec tous les processeurs logiques supplémentaires qui peuvent être ajoutés pendant l’exécution du système.

les groupes de processeurs Windows Server 2008, Windows Vista, Windows Server 2003 et Windows XP : groupes de processeurs ne sont pas pris en charge.

Chaque nœud doit être entièrement contenu dans un groupe. Si les capacités des nœuds sont relativement petites, le système affecte plusieurs nœuds au même groupe, en choisissant des nœuds physiquement proches les uns des autres pour de meilleures performances. Si la capacité d’un nœud dépasse le nombre maximal de processeurs d’un groupe, le système fractionne le nœud en plusieurs nœuds plus petits, chaque petite taille suffisante pour s’adapter à un groupe.

Un nœud NUMA idéal pour un nouveau processus peut être demandé à l’aide de l’attribut étendu PROC_THREAD_ATTRIBUTE_PREFERRED_NODE lors de la création du processus. Comme un processeur idéal de thread, le nœud idéal est un indicateur pour le planificateur, qui affecte le nouveau processus au groupe qui contient le nœud demandé si possible.

Les fonctions NUMA étendues GetNumaAvailableMemoryNodeEx, GetNumaNodeProcessorMaskEx, GetNumaProcessorNodeExet GetNumaProximityNodeEx diffèrent de leurs équivalents non chiffrés dans le fait que le nombre de nœuds est une valeur USHORT plutôt qu’un UCHAR, pour prendre en charge le plus grand nombre de nœuds sur un système avec plus de 64 processeurs logiques. En outre, le processeur spécifié avec ou récupéré par les fonctions étendues inclut le groupe de processeurs ; le processeur spécifié avec ou récupéré par les fonctions non en texte est relatif au groupe. Pour plus d’informations, consultez les rubriques de référence sur les fonctions individuelles.

Une application prenant en charge le groupe peut affecter tous ses threads à un nœud particulier de la même manière que celle décrite précédemment dans cette rubrique, à l’aide des fonctions NUMA étendues correspondantes. L’application utilise GetLogicalProcessorInformationEx pour obtenir la liste de tous les processeurs sur le système. Notez que l’application ne peut pas définir le masque d’affinité de processus, sauf si le processus est affecté à un seul groupe et que le nœud prévu se trouve dans ce groupe. En règle générale, l’application doit appeler SetThreadGroupAffinity pour limiter ses threads au nœud prévu.

Comportement à partir de Windows 10 Build 20348

Note

À compter de Windows 10 Build 20348, le comportement de cette fonction et d’autres fonctions NUMA a été modifié pour mieux prendre en charge les systèmes avec des nœuds contenant plus de 64 processeurs.

La création de nœuds « faux » pour prendre en charge un mappage 1:1 entre les groupes et les nœuds a entraîné des comportements déroutants où des nombres inattendus de nœuds NUMA sont signalés, et ainsi, à compter de Windows 10 Build 20348, le système d’exploitation a changé pour permettre à plusieurs groupes d’être associés à un nœud, et ainsi, la véritable topologie NUMA du système peut être signalée.

Dans le cadre de ces modifications apportées au système d’exploitation, un certain nombre d’API NUMA ont changé pour prendre en charge la création de rapports sur plusieurs groupes qui peuvent désormais être associés à un seul nœud NUMA. Les API mises à jour et les nouvelles API sont étiquetées dans le tableau de la section de l’API NUMA ci-dessous.

Étant donné que la suppression du fractionnement de nœud peut avoir un impact sur les applications existantes, une valeur de Registre est disponible pour permettre de revenir au comportement de fractionnement de nœud hérité. Le fractionnement de nœud peut être réactivé en créant une valeur REG_DWORD nommée « SplitLargeNodes » avec la valeur 1 sous HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\NUMA. Les modifications apportées à ce paramètre nécessitent un redémarrage.

reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\NUMA" /v SplitLargeNodes /t REG_DWORD /d 1

Note

Les applications mises à jour pour utiliser la nouvelle fonctionnalité d’API qui signale la véritable topologie NUMA continueront de fonctionner correctement sur les systèmes où le fractionnement de nœud volumineux a été réenable avec cette clé de Registre.

L’exemple suivant illustre d’abord les problèmes potentiels liés aux processeurs de mappage de tables sur des nœuds NUMA à l’aide des API d’affinité héritées, qui ne fournissent plus une couverture complète de tous les processeurs du système, ce qui peut entraîner une table incomplète. Les implications d’une telle incomplétité dépendent du contenu de la table. Si la table stocke simplement le numéro de nœud correspondant, il s’agit probablement uniquement d’un problème de performances avec les processeurs découverts laissés dans le cadre du nœud 0. Toutefois, si la table contient des pointeurs vers une structure de contexte par nœud, cela peut entraîner des déréférencements NULL au moment de l’exécution.

Ensuite, l’exemple de code illustre deux solutions de contournement pour le problème. La première consiste à migrer vers les API d’affinité entre plusieurs groupes (mode utilisateur et mode noyau). La deuxième consiste à utiliser KeQueryLogicalProcessorRelationship pour interroger directement le nœud NUMA associé à un numéro de processeur donné.


//
// Problematic implementation using KeQueryNodeActiveAffinity.
//

USHORT CurrentNode;
USHORT HighestNodeNumber;
GROUP_AFFINITY NodeAffinity;
ULONG ProcessorIndex;
PROCESSOR_NUMBER ProcessorNumber;

HighestNodeNumber = KeQueryHighestNodeNumber();
for (CurrentNode = 0; CurrentNode <= HighestNodeNumber; CurrentNode += 1) {

    KeQueryNodeActiveAffinity(CurrentNode, &NodeAffinity, NULL);
    while (NodeAffinity.Mask != 0) {

        ProcessorNumber.Group = NodeAffinity.Group;
        BitScanForward(&ProcessorNumber.Number, NodeAffinity.Mask);

        ProcessorIndex = KeGetProcessorIndexFromNumber(&ProcessorNumber);

        ProcessorNodeContexts[ProcessorIndex] = NodeContexts[CurrentNode;]

        NodeAffinity.Mask &= ~((KAFFINITY)1 << ProcessorNumber.Number);
    }
}

//
// Resolution using KeQueryNodeActiveAffinity2.
//

USHORT CurrentIndex;
USHORT CurrentNode;
USHORT CurrentNodeAffinityCount;
USHORT HighestNodeNumber;
ULONG MaximumGroupCount;
PGROUP_AFFINITY NodeAffinityMasks;
ULONG ProcessorIndex;
PROCESSOR_NUMBER ProcessorNumber;
NTSTATUS Status;

MaximumGroupCount = KeQueryMaximumGroupCount();
NodeAffinityMasks = ExAllocatePool2(POOL_FLAG_PAGED,
                                    sizeof(GROUP_AFFINITY) * MaximumGroupCount,
                                    'tseT');

if (NodeAffinityMasks == NULL) {
    return STATUS_NO_MEMORY;
}

HighestNodeNumber = KeQueryHighestNodeNumber();
for (CurrentNode = 0; CurrentNode <= HighestNodeNumber; CurrentNode += 1) {

    Status = KeQueryNodeActiveAffinity2(CurrentNode,
                                        NodeAffinityMasks,
                                        MaximumGroupCount,
                                        &CurrentNodeAffinityCount);
    NT_ASSERT(NT_SUCCESS(Status));

    for (CurrentIndex = 0; CurrentIndex < CurrentNodeAffinityCount; CurrentIndex += 1) {

        CurrentAffinity = &NodeAffinityMasks[CurrentIndex];

        while (CurrentAffinity->Mask != 0) {

            ProcessorNumber.Group = CurrentAffinity.Group;
            BitScanForward(&ProcessorNumber.Number, CurrentAffinity->Mask);

            ProcessorIndex = KeGetProcessorIndexFromNumber(&ProcessorNumber);

            ProcessorNodeContexts[ProcessorIndex] = NodeContexts[CurrentNode];

            CurrentAffinity->Mask &= ~((KAFFINITY)1 << ProcessorNumber.Number);
        }
    }
}

//
// Resolution using KeQueryLogicalProcessorRelationship.
//

ULONG ProcessorCount;
ULONG ProcessorIndex;
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX ProcessorInformation;
ULONG ProcessorInformationSize;
PROCESSOR_NUMBER ProcessorNumber;
NTSTATUS Status;

ProcessorCount = KeQueryActiveProcessorCountEx(ALL_PROCESSOR_GROUPS);

for (ProcessorIndex = 0; ProcessorIndex < ProcessorCount; ProcessorIndex += 1) {

    Status = KeGetProcessorNumberFromIndex(ProcessorIndex, &ProcessorNumber);
    NT_ASSERT(NT_SUCCESS(Status));

    ProcessorInformationSize = sizeof(ProcessorInformation);
    Status = KeQueryLogicalProcessorRelationship(&ProcessorNumber,
                                                    RelationNumaNode,
                                                    &ProcessorInformation,
                                                    &ProcesorInformationSize);
    NT_ASSERT(NT_SUCCESS(Status));

    NodeNumber = ProcessorInformation.NumaNode.NodeNumber;

    ProcessorNodeContexts[ProcessorIndex] = NodeContexts[NodeNumber];
}

NUMA API

Le tableau suivant décrit l’API NUMA.

Fonction Description
AllocateUserPhysicalPagesNuma Alloue des pages de mémoire physique à mapper et non mappées dans n’importe quelle extension de fenêtre d’adresse (AWE) d’un processus spécifié et spécifie le nœud NUMA pour la mémoire physique.
CreateFileMappingNuma Crée ou ouvre un objet de mappage de fichiers nommé ou non nommé pour un fichier spécifié et spécifie le nœud NUMA pour la mémoire physique.
GetLogicalProcessorInformation Mise à jour dans Windows 10 Build 20348. Récupère des informations sur les processeurs logiques et le matériel associé.
GetLogicalProcessorInformationEx Mise à jour dans Windows 10 Build 20348. Récupère des informations sur les relations des processeurs logiques et du matériel associé.
GetNumaAvailableMemoryNode Récupère la quantité de mémoire disponible dans le nœud spécifié.
GetNumaAvailableMemoryNodeEx Récupère la quantité de mémoire disponible dans un nœud spécifié sous la forme d’une valeur USHORT.
GetNumaHighestNodeNumber Récupère le nœud dont le nombre est actuellement le plus élevé.
GetNumaNodeProcessorMask Mise à jour dans Windows 10 Build 20348. Récupère le masque du processeur pour le nœud spécifié.
GetNumaNodeProcessorMask2 Nouveautés de Windows 10 Build 20348. Récupère le masque de processeur à plusieurs groupes du nœud spécifié.
GetNumaNodeProcessorMaskEx Mise à jour dans Windows 10 Build 20348. Récupère le masque de processeur d’un nœud spécifié sous la forme d’une valeur USHORT.
GetNumaProcessorNode Récupère le numéro de nœud du processeur spécifié.
GetNumaProcessorNodeEx Récupère le numéro de nœud sous la forme d’une valeur USHORT pour le processeur spécifié.
GetNumaProximityNode Récupère le numéro de nœud de l’identificateur de proximité spécifié.
GetNumaProximityNodeEx Récupère le numéro de nœud sous la forme d’une valeur USHORT pour l’identificateur de proximité spécifié.
GetProcessDefaultCpuSetMasks Nouveautés de Windows 10 Build 20348. Récupère la liste des jeux d’UC dans le jeu par défaut de processus défini par SetProcessDefaultCpuSetMasks ou SetProcessDefaultCpuSets.
GetThreadSelectedCpuSetMasks Nouveautés de Windows 10 Build 20348. Définit l’affectation des jeux d’UC sélectionnés pour le thread spécifié. Cette affectation remplace l’affectation par défaut du processus, si elle est définie.
mapViewOfFileExNuma Mappe une vue d’un mappage de fichiers dans l’espace d’adressage d’un processus appelant et spécifie le nœud NUMA pour la mémoire physique.
SetProcessDefaultCpuSetMasks Nouveautés de Windows 10 Build 20348. Définit l’affectation de jeux d’UC par défaut pour les threads dans le processus spécifié.
SetThreadSelectedCpuSetMasks Nouveautés de Windows 10 Build 20348. Définit l’affectation des jeux d’UC sélectionnés pour le thread spécifié. Cette affectation remplace l’affectation par défaut du processus, si elle est définie.
VirtualAllocExNuma Réserve ou valide une région de mémoire dans l’espace d’adressage virtuel du processus spécifié et spécifie le nœud NUMA pour la mémoire physique.

 

La fonction QueryWorkingSetEx peut être utilisée pour récupérer le nœud NUMA sur lequel une page est allouée. Pour obtenir un exemple, consultez allocation de mémoire à partir d’un nœud NUMA.

allocation de mémoire à partir d’un nœud NUMA

plusieurs processeurs

groupes de processeurs