Udostępnij za pośrednictwem


Architektura architektury NUMA

Tradycyjny model architektury wieloprocesorowej to symetryczny wieloprocesor (SMP). W tym modelu każdy procesor ma równy dostęp do pamięci i operacji we/wy. W miarę dodawania większej liczby procesorów magistrala procesorów staje się ograniczeniem wydajności systemu.

Projektanci systemu używają nieuprzyzwoity dostęp do pamięci (NUMA), aby zwiększyć szybkość procesora bez zwiększania obciążenia magistrali procesora. Architektura nie jest jednolita, ponieważ każdy procesor znajduje się blisko niektórych części pamięci i dalej od innych części pamięci. Procesor szybko uzyskuje dostęp do pamięci, z którą jest blisko, a uzyskanie dostępu do pamięci, która jest dalej, może potrwać dłużej.

W systemie NUMA procesory CPU są rozmieszczone w mniejszych systemach nazywanych węzłami . Każdy węzeł ma własne procesory i pamięć i jest połączony z większym systemem za pośrednictwem spójnej w pamięci podręcznej magistrali połączeń.

System próbuje zwiększyć wydajność, planując wątki na procesorach, które znajdują się w tym samym węźle co używana pamięć. Próbuje spełnić żądania alokacji pamięci z węzła, ale w razie potrzeby przydzieli pamięć z innych węzłów. Udostępnia również interfejs API umożliwiający udostępnienie topologii systemu aplikacjom. Wydajność aplikacji można poprawić przy użyciu funkcji NUMA, aby zoptymalizować planowanie i użycie pamięci.

Przede wszystkim należy określić układ węzłów w systemie. Aby pobrać najwyższy numerowany węzeł w systemie, użyj funkcji GetNumaHighestNodeNumber. Należy pamiętać, że ta liczba nie jest gwarantowana, aby równała się całkowitej liczbie węzłów w systemie. Ponadto węzły z liczbami sekwencyjnymi nie mają gwarancji, że będą blisko siebie. Aby pobrać listę procesorów w systemie, użyj funkcji GetProcessAffinityMask. Węzeł dla każdego procesora na liście można określić przy użyciu funkcji GetNumaProcessorNode. Alternatywnie, aby pobrać listę wszystkich procesorów w węźle, użyj funkcji GetNumaNodeProcessorMask.

Po określeniu, które procesory należą do których węzłów, można zoptymalizować wydajność aplikacji. Aby upewnić się, że wszystkie wątki procesu są uruchamiane w tym samym węźle, użyj funkcji SetProcessAffinityMask z maską koligacji procesu, która określa procesory w tym samym węźle. Zwiększa to wydajność aplikacji, których wątki muszą uzyskiwać dostęp do tej samej pamięci. Alternatywnie, aby ograniczyć liczbę wątków w każdym węźle, użyj funkcji SetThreadAffinityMask.

Aplikacje intensywnie korzystające z pamięci będą musiały zoptymalizować użycie pamięci. Aby pobrać ilość wolnej pamięci dostępnej dla węzła, użyj funkcji GetNumaAvailableMemoryNode. Funkcja VirtualAllocExNuma umożliwia aplikacji określenie preferowanego węzła dla alokacji pamięci. VirtualAllocExNuma nie przydziela żadnych stron fizycznych, więc powiedzie się, czy strony są dostępne w tym węźle, czy w innym miejscu w systemie. Strony fizyczne są przydzielane na żądanie. Jeśli preferowany węzeł zabraknie stron, menedżer pamięci będzie używać stron z innych węzłów. Jeśli pamięć jest stronicowana, ten sam proces jest używany, gdy zostanie przywrócony.

Obsługa architektury NUMA w systemach z ponad 64 procesorami logicznymi

W systemach z ponad 64 procesorami logicznymi węzły są przypisywane do grup procesorów zgodnie z pojemnością węzłów. Pojemność węzła to liczba procesorów, które są obecne, gdy system rozpoczyna się wraz z dodatkowymi procesorami logicznymi, które można dodać podczas uruchamiania systemu.

Windows Server 2008, Windows Vista, Windows Server 2003 i Windows XP: grupy procesorów nie są obsługiwane.

Każdy węzeł musi być w pełni zawarty w grupie. Jeśli pojemności węzłów są stosunkowo małe, system przypisuje więcej niż jeden węzeł do tej samej grupy, wybierając węzły, które są fizycznie blisko siebie, aby uzyskać lepszą wydajność. Jeśli pojemność węzła przekracza maksymalną liczbę procesorów w grupie, system dzieli węzeł na wiele mniejszych węzłów, każdy wystarczająco mały, aby zmieścić się w grupie.

Idealny węzeł NUMA dla nowego procesu można zażądać przy użyciu atrybutu rozszerzonego PROC_THREAD_ATTRIBUTE_PREFERRED_NODE podczas tworzenia procesu. Podobnie jak w przypadku procesora idealnego wątku, idealny węzeł jest wskazówką dla harmonogramu, która przypisuje nowy proces do grupy zawierającej żądany węzeł, jeśli to możliwe.

Rozszerzone funkcje NUMA GetNumaAvailableMemoryNodeEx, GetNumaNodeProcessorMaskEx, GetNumaProcessorNodeExi GetNumaProximityNodeNode ex różnią się od ich nieusuniętych odpowiedników w tym, że numer węzła jest wartością USHORT, a nie wartością UCHAR, aby pomieścić potencjalnie większą liczbę węzłów w systemie z ponad 64 procesorami logicznymi. Ponadto procesor określony za pomocą lub pobrany przez funkcje rozszerzone obejmuje grupę procesorów; procesor określony z lub pobrany przez funkcje nieekstenowane jest względny grupy. Aby uzyskać szczegółowe informacje, zobacz tematy referencyjne poszczególnych funkcji.

Aplikacja z obsługą grup może przypisać wszystkie jego wątki do określonego węzła w podobny sposób do opisanego wcześniej w tym temacie przy użyciu odpowiednich rozszerzonych funkcji NUMA. Aplikacja używa GetLogicalProcessorInformationEx, aby uzyskać listę wszystkich procesorów w systemie. Należy pamiętać, że aplikacja nie może ustawić maski koligacji procesu, chyba że proces jest przypisany do jednej grupy, a zamierzony węzeł znajduje się w tej grupie. Zazwyczaj aplikacja musi wywoływać SetThreadGroupAffinity, aby ograniczyć jego wątki do zamierzonego węzła.

Zachowanie począwszy od systemu Windows 10 Build 20348

Nuta

Począwszy od systemu Windows 10 Build 20348, zachowanie tej i innych funkcji NUMA zostało zmodyfikowane w celu lepszej obsługi systemów z węzłami zawierającymi więcej niż 64 procesory.

Tworzenie "fałszywych" węzłów do obsługi mapowania 1:1 między grupami i węzłami spowodowało mylące zachowania, w których zgłoszono nieoczekiwaną liczbę węzłów NUMA, a więc począwszy od systemu Windows 10 Build 20348 system operacyjny zmienił się, aby umożliwić skojarzenie wielu grup z węzłem, a więc teraz można zgłosić prawdziwą topologię NUMA systemu.

W ramach tych zmian w systemie operacyjnym zmieniono wiele interfejsów API NUMA w celu obsługi raportowania wielu grup, które można teraz skojarzyć z jednym węzłem NUMA. Zaktualizowane i nowe interfejsy API są oznaczone w tabeli w poniższej sekcji interfejsu API NUMA.

Ponieważ usunięcie podziału węzła może potencjalnie mieć wpływ na istniejące aplikacje, dostępna jest wartość rejestru umożliwiająca powrót do starszego zachowania podziału węzła. Dzielenie węzłów można ponownie włączyć, tworząc wartość REG_DWORD o nazwie "SplitLargeNodes" z wartością 1 poniżej HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\NUMA. Zmiany w tym ustawieniu wymagają ponownego uruchomienia, aby zaczęły obowiązywać.

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

Nuta

Aplikacje zaktualizowane do korzystania z nowej funkcji interfejsu API, które zgłaszają prawdziwą topologię NUMA, będą nadal działać prawidłowo w systemach, w których duże dzielenie węzłów zostało ponownie włączone z tym kluczem rejestru.

W poniższym przykładzie przedstawiono najpierw potencjalne problemy z kompilacjami tabel mapowania procesorów na węzły NUMA przy użyciu starszych interfejsów API koligacji, które nie zapewniają już pełnej ochrony wszystkich procesorów w systemie, co może spowodować niekompletną tabelę. Implikacje takiej niekompletności zależą od zawartości tabeli. Jeśli tabela po prostu przechowuje odpowiedni numer węzła, prawdopodobnie jest to problem z wydajnością tylko wykrytych procesorów pozostawionych w ramach węzła 0. Jeśli jednak tabela zawiera wskaźniki do struktury kontekstu dla węzła, może to spowodować wyłudzenia wartości NULL w czasie wykonywania.

Następnie przykładowy kod ilustruje dwa obejścia problemu. Pierwszą z nich jest migracja do interfejsów API koligacji węzłów wielogrupowych (tryb użytkownika i tryb jądra). Drugim jest użycie KeQueryLogicalProcessorRelationship, aby bezpośrednio wysłać zapytanie do węzła NUMA skojarzonego z danym numerem procesora.


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

W poniższej tabeli opisano interfejs API NUMA.

Funkcja Opis
PrzydzieluserPhysicalPagesNuma Przydziela strony pamięci fizycznej do mapowania i rozpakowania w dowolnym Rozszerzenia okien adresowych (AWE) regionu określonego procesu i określa węzeł NUMA dla pamięci fizycznej.
CreateFileMappingNuma Tworzy lub otwiera nazwany lub nienazwany obiekt mapowania pliku dla określonego pliku i określa węzeł NUMA dla pamięci fizycznej.
GetLogicalProcessorInformation Zaktualizowano w systemie Windows 10 Build 20348. Pobiera informacje o procesorach logicznych i powiązanym sprzęcie.
GetLogicalProcessorInformationEx Zaktualizowano w systemie Windows 10 Build 20348. Pobiera informacje o relacjach procesorów logicznych i powiązanego sprzętu.
GetNumaAvailableMemoryNode Pobiera ilość pamięci dostępnej w określonym węźle.
getNumaAvailableMemoryNodeEx Pobiera ilość pamięci dostępnej w węźle określonym jako wartość USHORT.
GetNumaHighestNodeNumber Pobiera węzeł, który ma obecnie największą liczbę.
GetNumaNodeProcessorMask Zaktualizowano w systemie Windows 10 Build 20348. Pobiera maskę procesora dla określonego węzła.
GetNumaNodeProcessorMask2 Nowość w systemie Windows 10 Build 20348. Pobiera maskę procesora wielogrupowego określonego węzła.
GetNumaNodeProcessorMaskEx Zaktualizowano w systemie Windows 10 Build 20348. Pobiera maskę procesora dla węzła określonego jako wartość USHORT.
GetNumaProcessorNode Pobiera numer węzła dla określonego procesora.
GetNumaProcessorNodeEx Pobiera numer węzła jako wartość USHORT dla określonego procesora.
getNumaProximityNode Pobiera numer węzła dla określonego identyfikatora zbliżeniowego.
getNumaProximityNodeEx Pobiera numer węzła jako wartość USHORT dla określonego identyfikatora zbliżeniowego.
GetProcessDefaultCpuSetMasks Nowość w systemie Windows 10 Build 20348. Pobiera listę zestawów procesora CPU w domyślnym zestawie procesów ustawionym przez setProcessDefaultCpuSetMasks lub SetProcessDefaultCpuSets.
GetThreadSelectedCpuSetMasks Nowość w systemie Windows 10 Build 20348. Ustawia wybrane przypisanie zestawów procesora CPU dla określonego wątku. To przypisanie zastępuje domyślne przypisanie procesu, jeśli jest ustawione.
MapViewOfFileExNuma Mapuje widok mapowania pliku na przestrzeń adresową procesu wywołującego i określa węzeł NUMA dla pamięci fizycznej.
SetProcessDefaultCpuSetMasks Nowość w systemie Windows 10 Build 20348. Ustawia domyślne przypisanie zestawów procesora CPU dla wątków w określonym procesie.
SetThreadSelectedCpuSetMasks Nowość w systemie Windows 10 Build 20348. Ustawia wybrane przypisanie zestawów procesora CPU dla określonego wątku. To przypisanie zastępuje domyślne przypisanie procesu, jeśli jest ustawione.
VirtualAllocExNuma Rezerwuje lub zatwierdza region pamięci w wirtualnej przestrzeni adresowej określonego procesu i określa węzeł NUMA dla pamięci fizycznej.

 

Funkcja QueryWorkingSetEx może służyć do pobierania węzła NUMA, na którym jest przydzielana strona. Aby zapoznać się z przykładem, zobacz przydzielanie pamięci z węzła NUMA.

przydzielanie pamięci z węzła NUMA

wielu procesorów

grupy procesorów