NUMA-Architektur

Das herkömmliche Modell für die Multiprozessorarchitektur ist symmetrischer Multiprozessor (SMP). In diesem Modell hat jeder Prozessor den gleichen Zugriff auf Arbeitsspeicher und E/A. Wenn weitere Prozessoren hinzugefügt werden, wird der Prozessorbus zu einer Einschränkung für die Systemleistung.

Systemdesigner verwenden den nicht uniformen Arbeitsspeicherzugriff (NUMA), um die Prozessorgeschwindigkeit zu erhöhen, ohne die Auslastung des Prozessorbusses zu erhöhen. Die Architektur ist nicht einheitlich, da sich jeder Prozessor in der Nähe einiger Teile des Arbeitsspeichers und weiter von anderen Teilen des Arbeitsspeichers entfernt. Der Prozessor erhält schnell Zugriff auf den Arbeitsspeicher, an dem er sich in der Nähe befindet, während es länger dauern kann, bis er zugriff auf den weiter entfernten Arbeitsspeicher erhält.

In einem NUMA-System werden CPUs in kleineren Systemen angeordnet, die als Knoten bezeichnet werden. Jeder Knoten verfügt über eigene Prozessoren und Arbeitsspeicher und ist über einen Cache-kohärenten Verbindungsbus mit dem größeren System verbunden.

Das System versucht, die Leistung zu verbessern, indem Threads für Prozessoren geplant werden, die sich im selben Knoten wie der verwendete Arbeitsspeicher befinden. Es versucht, Speicherzuordnungsanforderungen aus dem Knoten zu erfüllen, ordnet aber bei Bedarf Arbeitsspeicher von anderen Knoten zu. Außerdem stellt es eine API bereit, um die Topologie des Systems für Anwendungen verfügbar zu machen. Sie können die Leistung Ihrer Anwendungen verbessern, indem Sie die NUMA-Funktionen verwenden, um die Planung und Die Speicherauslastung zu optimieren.

Zunächst müssen Sie das Layout der Knoten im System bestimmen. Um den höchsten nummerierten Knoten im System abzurufen, verwenden Sie die GetNumaHighestNodeNumber-Funktion . Beachten Sie, dass diese Zahl nicht garantiert der Gesamtanzahl von Knoten im System entspricht. Außerdem ist nicht garantiert, dass Knoten mit sequenziellen Zahlen eng beieinander liegen. Um die Liste der Prozessoren im System abzurufen, verwenden Sie die GetProcessAffinityMask-Funktion . Sie können den Knoten für jeden Prozessor in der Liste ermitteln, indem Sie die GetNumaProcessorNode-Funktion verwenden. Alternativ können Sie die GetNumaNodeProcessorMask-Funktion verwenden, um eine Liste aller Prozessoren in einem Knoten abzurufen.

Nachdem Sie ermittelt haben, welche Prozessoren zu welchen Knoten gehören, können Sie die Leistung Ihrer Anwendung optimieren. Um sicherzustellen, dass alle Threads für Ihren Prozess auf demselben Knoten ausgeführt werden, verwenden Sie die SetProcessAffinityMask-Funktion mit einer Prozessaffinitätsmaske, die Prozessoren im gleichen Knoten angibt. Dies erhöht die Effizienz von Anwendungen, deren Threads auf denselben Arbeitsspeicher zugreifen müssen. Alternativ können Sie die SetThreadAffinityMask-Funktion verwenden, um die Anzahl der Threads auf jedem Knoten zu begrenzen.

Speicherintensive Anwendungen müssen ihre Speicherauslastung optimieren. Verwenden Sie die GetNumaAvailableMemoryNode-Funktion , um die Für einen Knoten verfügbare Menge an freiem Arbeitsspeicher abzurufen. Mit der VirtualAllocExNuma-Funktion kann die Anwendung einen bevorzugten Knoten für die Speicherbelegung angeben. VirtualAllocExNuma weist keine physischen Seiten zu, sodass es erfolgreich ist, unabhängig davon, ob die Seiten auf diesem Knoten oder an anderer Stelle im System verfügbar sind oder nicht. Die physischen Seiten werden bei Bedarf zugeordnet. Wenn für den bevorzugten Knoten keine Seiten mehr vorhanden sind, verwendet der Speicher-Manager Seiten von anderen Knoten. Wenn der Speicher ausgelagert wird, wird der gleiche Prozess verwendet, wenn er wieder eingeholt wird.

NUMA-Unterstützung für Systeme mit mehr als 64 logischen Prozessoren

Auf Systemen mit mehr als 64 logischen Prozessoren werden Knoten prozessorgruppen entsprechend der Kapazität der Knoten zugewiesen. Die Kapazität eines Knotens ist die Anzahl von Prozessoren, die vorhanden sind, wenn das System zusammen mit zusätzlichen logischen Prozessoren gestartet wird, die während der Systemausführung hinzugefügt werden können.

Windows Server 2008, Windows Vista, Windows Server 2003 und Windows XP: Prozessorgruppen werden nicht unterstützt.

Jeder Knoten muss vollständig in einer Gruppe enthalten sein. Wenn die Kapazitäten der Knoten relativ klein sind, weist das System der gleichen Gruppe mehr als einen Knoten zu und wählt Knoten aus, die sich physisch nahe beieinander befinden, um eine bessere Leistung zu erzielen. Wenn die Kapazität eines Knotens die maximale Anzahl von Prozessoren in einer Gruppe überschreitet, teilt das System den Knoten in mehrere kleinere Knoten auf, die jeweils klein genug sind, um in eine Gruppe zu passen.

Ein idealer NUMA-Knoten für einen neuen Prozess kann mit dem PROC_THREAD_ATTRIBUTE_PREFERRED_NODE erweiterten Attribut angefordert werden, wenn der Prozess erstellt wird. Wie ein idealer Threadprozessor ist der ideale Knoten ein Hinweis an den Planer, der den neuen Prozess der Gruppe zuweist, die den angeforderten Knoten enthält, wenn möglich.

Die erweiterten NUMA-Funktionen GetNumaAvailableMemoryNodeEx, GetNumaNodeProcessorMaskEx, GetNumaProcessorNodeEx und GetNumaProximityNodeEx unterscheiden sich von ihren unbeaufsichtigten Gegenstücken darin, dass die Knotennummer ein USHORT-Wert und keine UCHAR ist, um die potenziell größere Anzahl von Knoten auf einem System mit mehr als 64 logischen Prozessoren aufzunehmen. Außerdem umfasst der Prozessor, der mit den erweiterten Funktionen angegeben oder von diesem abgerufen wird, die Prozessorgruppe; Der Prozessor, der mit den unbeaufsichtigten Funktionen angegeben oder von den unbeaufsichtigten Funktionen abgerufen wird, ist gruppenrelativ. Ausführliche Informationen finden Sie in den Referenzthemen zu den einzelnen Funktionen.

Eine gruppenfähige Anwendung kann alle zugehörigen Threads einem bestimmten Knoten auf ähnliche Weise wie zuvor in diesem Thema beschrieben zuweisen, indem sie die entsprechenden erweiterten NUMA-Funktionen verwendet. Die Anwendung verwendet GetLogicalProcessorInformationEx , um die Liste aller Prozessoren im System abzurufen. Beachten Sie, dass die Anwendung die Prozessaffinitätsmaske nur festlegen kann, wenn der Prozess einer einzelnen Gruppe zugewiesen ist und sich der beabsichtigte Knoten in dieser Gruppe befindet. Normalerweise muss die Anwendung SetThreadGroupAffinity aufrufen, um ihre Threads auf den beabsichtigten Knoten zu beschränken.

Verhalten ab Windows 10 Build 20348

Hinweis

Ab Windows 10 Build 20348 wurde das Verhalten dieser und anderer NUMA-Funktionen geändert, um Systeme mit Knoten mit mehr als 64 Prozessoren besser zu unterstützen.

Das Erstellen von "gefälschten" Knoten zum Aufnehmen einer 1:1-Zuordnung zwischen Gruppen und Knoten hat zu verwirrenden Verhaltensweisen geführt, bei denen unerwartete Zahlen von NUMA-Knoten gemeldet werden, und so wurde das Betriebssystem ab Windows 10 Build 20348 geändert, sodass mehrere Gruppen einem Knoten zugeordnet werden können, sodass jetzt die wahre NUMA-Topologie des Systems gemeldet werden kann.

Im Rahmen dieser Änderungen am Betriebssystem wurde eine Reihe von NUMA-APIs geändert, um die Berichterstellung für mehrere Gruppen zu unterstützen, die nun einem einzelnen NUMA-Knoten zugeordnet werden können. Aktualisierte und neue APIs werden in der Tabelle im Abschnitt NUMA-API unten bezeichnet.

Da sich das Entfernen der Knotenaufteilung möglicherweise auf vorhandene Anwendungen auswirken kann, ist ein Registrierungswert verfügbar, um die Zurücknahme auf das Ältere Knotenteilungsverhalten zu ermöglichen. Die Knotenaufteilung kann erneut aktiviert werden, indem ein REG_DWORD Wert namens "SplitLargeNodes" mit dem Wert 1 unter HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\NUMA erstellt wird. Änderungen dieser Einstellung werden erst nach einem Neustart wirksam.

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

Hinweis

Anwendungen, die aktualisiert wurden, um die neue API-Funktionalität zu verwenden, die die wahre NUMA-Topologie meldet, funktionieren weiterhin ordnungsgemäß auf Systemen, auf denen die Aufteilung großer Knoten mit diesem Registrierungsschlüssel erneut aktiviert wurde.

Das folgende Beispiel zeigt zunächst potenzielle Probleme bei Der Erstellung von Tabellen, die PROZESSOREN zu NUMA-Knoten unter Verwendung der Legacyaffinitäts-APIs zuordnen, die nicht mehr eine vollständige Abdeckung aller Prozessoren im System bieten, was zu einer unvollständigen Tabelle führen kann. Welche Auswirkungen eine solche Unvollständigkeit hat, hängt vom Inhalt der Tabelle ab. Wenn die Tabelle einfach die entsprechende Knotennummer speichert, handelt es sich wahrscheinlich nur um ein Leistungsproblem, bei dem nicht ermittelte Prozessoren als Teil von Knoten 0 verbleiben. Wenn die Tabelle jedoch Zeiger auf eine Kontextstruktur pro Knoten enthält, kann dies zur Laufzeit zu NULL-Rückschlüssen führen.

Als Nächstes werden im Codebeispiel zwei Problemumgehungen für das Problem veranschaulicht. Die erste besteht darin, zu den AFFINITY-APIs für mehrere Gruppen (Benutzermodus und Kernelmodus) zu migrieren. Die zweite besteht darin, KeQueryLogicalProcessorRelationship zu verwenden, um den NUMA-Knoten, der einer bestimmten Prozessornummer zugeordnet ist, direkt abzufragen.


//
// 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

In der folgenden Tabelle wird die NUMA-API beschrieben.

Funktion BESCHREIBUNG
AllocateUserPhysicalPagesNuma Weist seiten des physischen Arbeitsspeichers zu, die innerhalb eines AWE-Bereichs ( Address Windowing Extensions ) eines angegebenen Prozesses zugeordnet und aufgehoben werden sollen, und gibt den NUMA-Knoten für den physischen Arbeitsspeicher an.
CreateFileMappingNuma Erstellt oder öffnet ein benanntes oder unbenannte Dateizuordnungsobjekt für eine angegebene Datei und gibt den NUMA-Knoten für den physischen Arbeitsspeicher an.
GetLogicalProcessorInformation Aktualisiert in Windows 10 Build 20348. Ruft Informationen zu logischen Prozessoren und zugehöriger Hardware ab.
GetLogicalProcessorInformationEx Aktualisiert in Windows 10 Build 20348. Ruft Informationen zu den Beziehungen logischer Prozessoren und zugehöriger Hardware ab.
GetNumaAvailableMemoryNode Ruft die Menge an Arbeitsspeicher ab, die im angegebenen Knoten verfügbar ist.
GetNumaAvailableMemoryNodeEx Ruft die Menge des verfügbaren Arbeitsspeichers in einem Knoten ab, der als USHORT-Wert angegeben ist.
GetNumaHighestNodeNumber Ruft den Knoten ab, der derzeit die höchste Anzahl aufweist.
GetNumaNodeProcessorMask Aktualisiert in Windows 10 Build 20348. Ruft die Prozessormaske für den angegebenen Knoten ab.
GetNumaNodeProcessorMask2 Neu in Windows 10 Build 20348. Ruft die Prozessormaske für mehrere Gruppen des angegebenen Knotens ab.
GetNumaNodeProcessorMaskEx Aktualisiert in Windows 10 Build 20348. Ruft die Prozessormaske für einen Knoten ab, der als USHORT-Wert angegeben ist.
GetNumaProcessorNode Ruft die Knotennummer für den angegebenen Prozessor ab.
GetNumaProcessorNodeEx Ruft die Knotennummer als USHORT-Wert für den angegebenen Prozessor ab.
GetNumaProximityNode Ruft die Knotennummer für den angegebenen Näherungsbezeichner ab.
GetNumaProximityNodeEx Ruft die Knotennummer als USHORT-Wert für den angegebenen Näherungsbezeichner ab.
GetProcessDefaultCpuSetMasks Neu in Windows 10 Build 20348. Ruft die Liste der CPU-Sätze im Prozessstandardsatz ab, der von SetProcessDefaultCpuSetMasks oder SetProcessDefaultCpuSets festgelegt wurde.
GetThreadSelectedCpuSetMasks Neu in Windows 10 Build 20348. Legt die ausgewählte CPU-Sätze-Zuweisung für den angegebenen Thread fest. Diese Zuweisung setzt die Standardzuweisung des Prozesses außer Kraft, wenn eine festgelegt ist.
MapViewOfFileExNuma Ordnet eine Ansicht einer Dateizuordnung dem Adressraum eines aufrufenden Prozesses zu und gibt den NUMA-Knoten für den physischen Arbeitsspeicher an.
SetProcessDefaultCpuSetMasks Neu in Windows 10 Build 20348. Legt die Standardzuweisung für CPU-Sätze für Threads im angegebenen Prozess fest.
SetThreadSelectedCpuSetMasks Neu in Windows 10 Build 20348. Legt die ausgewählte CPU-Sätze-Zuweisung für den angegebenen Thread fest. Diese Zuweisung setzt die Standardzuweisung des Prozesses außer Kraft, wenn eine festgelegt ist.
VirtualAllocExNuma Reserviert oder committent eine Speicherregion innerhalb des virtuellen Adressraums des angegebenen Prozesses und gibt den NUMA-Knoten für den physischen Arbeitsspeicher an.

 

Die QueryWorkingSetEx-Funktion kann verwendet werden, um den NUMA-Knoten abzurufen, auf dem eine Seite zugeordnet ist. Ein Beispiel finden Sie unter Zuweisen von Arbeitsspeicher über einen NUMA-Knoten.

Zuweisung von Arbeitsspeicher über einen NUMA-Knoten

Mehrere Prozessoren

Prozessorgruppen