NUMA Architecture

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

Les concepteurs de systèmes 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 plus éloigné des autres parties de la mémoire. Le processeur accède rapidement à la mémoire dont il est proche, alors qu’il peut prendre plus de temps pour accéder à la mémoire qui est 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 plus grand système par le biais d’un bus d’interconnexion cohérent dans 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 répondre aux demandes d’allocation de mémoire à partir du nœud, mais allouera la mémoire 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é du 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, la proximité des nœuds avec des nombres séquentiels n’est pas garantie. Pour récupérer la liste des processeurs sur le système, utilisez la fonction GetProcessAffinityMask . Vous pouvez déterminer le nœud pour chaque processeur de la liste à l’aide de la fonction GetNumaProcessorNode . Pour récupérer une liste de tous les processeurs d’un nœud, utilisez la fonction GetNumaNodeProcessorMask .

Une fois que vous avez déterminé quels processeurs appartiennent à quels 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. Vous pouvez également utiliser la fonction SetThreadAffinityMask pour limiter le nombre de threads sur chaque nœud.

Les applications gourmandes en mémoire devront optimiser leur utilisation de la mémoire. Pour récupérer la quantité de mémoire libre disponible sur 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 les pages d’autres nœuds. Si la mémoire est paginée, le même processus est utilisé lorsqu’il est remis en service.

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 attribués à des groupes de processeurs en fonction de la capacité des nœuds. La capacité d’un nœud correspond au 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.

Windows Server 2008, Windows Vista, Windows Server 2003 et Windows XP : Les 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 dans un groupe, le système fractionne le nœud en plusieurs nœuds plus petits, chacun suffisamment petit 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, GetNumaProcessorNodeEx et GetNumaProximityNodeEx diffèrent de leurs homologues sans texte en ce que le numéro de nœud est une valeur USHORT plutôt qu’un UCHAR, pour prendre en charge le nombre potentiellement plus élevé 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 sans 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

Notes

À 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 « faux » nœuds pour prendre en charge un mappage 1:1 entre les groupes et les nœuds a entraîné des comportements confus où des nombres inattendus de nœuds NUMA sont signalés. Par conséquent, à 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 la topologie NUMA réelle du système peut donc maintenant être signalée.

Dans le cadre de ces modifications apportées au système d’exploitation, un certain nombre d’API NUMA ont été modifiées 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 API NUMA ci-dessous.

Étant donné que la suppression du fractionnement des nœuds 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 des nœuds 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. Une modification de ce paramètre exige un redémarrage.

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

Notes

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

L’exemple suivant illustre d’abord les problèmes potentiels liés aux builds tables mappant des processeurs à 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étude dépendent du contenu du tableau. Si la table stocke simplement le numéro de nœud correspondant, il s’agit probablement uniquement d’un problème de performances avec des processeurs découverts restant 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é de nœud multi-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 à démapper dans n’importe quelle région AWE ( Address Windowing Extensions ) 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 entre les processeurs logiques et le 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 qui a actuellement le nombre le plus élevé.
GetNumaNodeProcessorMask Mise à jour dans Windows 10 Build 20348. Récupère le masque de 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 pour un nœud spécifié en tant que 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 pour 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 ensembles de processeurs dans le jeu de processus par défaut qui a été défini par SetProcessDefaultCpuSetMasks ou SetProcessDefaultCpuSets.
GetThreadSelectedCpuSetMasks Nouveautés de Windows 10 build 20348. Définit l’affectation de jeux d’UC sélectionné pour le thread spécifié. Cette affectation remplace l’affectation par défaut de processus, le cas échéant.
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 par défaut des ensembles d’uc pour les threads dans le processus spécifié.
SetThreadSelectedCpuSetMasks Nouveautés de Windows 10 build 20348. Définit l’affectation de jeux d’UC sélectionné pour le thread spécifié. Cette affectation remplace l’affectation par défaut de processus, le cas échéant.
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