Sdílet prostřednictvím


Architektura NUMA

Tradiční model pro architekturu multiprocesoru je symetrický multiprocesor (SMP). V tomto modelu má každý procesor stejný přístup k paměti a vstupně-výstupním operacím. Při přidání dalších procesorů se sběrnice procesoru stává omezením výkonu systému.

Návrháři systému používají ne uniformní přístup k paměti (NUMA), aby zvýšili rychlost procesoru, aniž by zvýšili zatížení sběrnice procesoru. Architektura není jednotná, protože každý procesor je blízko k některým částem paměti a dále od jiných částí paměti. Procesor rychle získá přístup k paměti, ke které je blízko, zatímco může trvat delší dobu, než získá přístup k paměti, která je daleko daleko.

V systému NUMA jsou procesory uspořádány v menších systémech označovaných jako uzly. Každý uzel má vlastní procesory a paměť a je připojený k většímu systému prostřednictvím sběrnice propojené mezipamětí.

Systém se pokusí zvýšit výkon naplánováním vláken na procesorech, které jsou ve stejném uzlu jako používaná paměť. V případě potřeby se pokusí splnit požadavky na přidělení paměti z uzlu, ale v případě potřeby přidělí paměť z jiných uzlů. Poskytuje také rozhraní API pro zpřístupnění topologie systému pro aplikace. Výkon aplikací můžete zlepšit pomocí funkcí NUMA pro optimalizaci plánování a využití paměti.

Nejprve budete muset určit rozložení uzlů v systému. Pokud chcete načíst nejvyšší číslovaný uzel v systému, použijte funkci GetNumaHighestNodeNumber. Všimněte si, že toto číslo není zaručeno, že se rovná celkovému počtu uzlů v systému. Uzly se sekvenčními čísly také nejsou zaručené, že budou blízko sebe. K načtení seznamu procesorů v systému použijte funkci GetProcessAffinityMask. Uzel pro každý procesor v seznamu můžete určit pomocí funkce GetNumaProcessorNode. Pokud chcete také načíst seznam všech procesorů v uzlu, použijte funkci GetNumaNodeProcessorMask.

Jakmile určíte, které procesory patří do kterých uzlů, můžete optimalizovat výkon aplikace. Pokud chcete zajistit, aby se všechna vlákna procesu spouštěla na stejném uzlu, použijte funkci SetProcessAffinityMas k s maskou spřažení procesu, která určuje procesory ve stejném uzlu. To zvyšuje efektivitu aplikací, jejichž vlákna potřebují přístup ke stejné paměti. Pokud chcete omezit počet vláken na každém uzlu, použijte funkci SetThreadAffinityMask.

Aplikace náročné na paměť budou muset optimalizovat využití paměti. Pokud chcete načíst množství volné paměti dostupné pro uzel, použijte funkci GetNumaAvailableMemoryNode. Funkce VirtualAllocExNuma umožňuje aplikaci určit upřednostňovaný uzel pro přidělení paměti. VirtualAllocExNuma nepřiděluje žádné fyzické stránky, takže bude úspěšné, jestli jsou stránky dostupné na daném uzlu nebo jinde v systému. Fyzické stránky se přidělují na vyžádání. Pokud upřednostňovaný uzel narazí na stránky, správce paměti použije stránky z jiných uzlů. Pokud je paměť stránkovaná, použije se stejný proces, když se vrátí zpět.

Podpora NUMA v systémech s více než 64 logickými procesory

V systémech s více než 64 logickými procesory jsou uzly přiřazeny ke skupinám procesorů podle kapacity uzlů. Kapacita uzlu je počet procesorů, které jsou přítomné při spuštění systému společně s dalšími logickými procesory, které je možné přidat, když je systém spuštěný.

Windows Server 2008, Windows Vista, Windows Server 2003 a Windows XP: skupiny procesorů nejsou podporovány.

Každý uzel musí být plně obsažený ve skupině. Pokud jsou kapacity uzlů relativně malé, systém přiřadí ke stejné skupině více než jeden uzel a pro lepší výkon zvolí uzly, které jsou fyzicky blízko sebe navzájem. Pokud kapacita uzlu překročí maximální počet procesorů ve skupině, systém rozdělí uzel na několik menších uzlů, přičemž každý z nich je dostatečně malý, aby se vešl do skupiny.

Ideální uzel NUMA pro nový proces lze požadovat pomocí PROC_THREAD_ATTRIBUTE_PREFERRED_NODE rozšířeného atributu při vytvoření procesu. Stejně jako procesor ideální pro vlákno je ideální uzel tip plánovače, který přiřadí nový proces skupině, která obsahuje požadovaný uzel, pokud je to možné.

Rozšířené funkce NUMA GetNumaAvailableMemoryNodeEx, GetNumaNodeProcessorMaskEx, GetNumaProcessorNodeExa GetNumaProximityNodeEx se liší od jejich nezamýšlených protějšků v tom, že číslo uzlu je hodnota USHORT místo UCHAR, aby se přizpůsobil potenciálně většímu počtu uzlů v systému s více než 64 logickými procesory. Procesor určený nebo načtený rozšířenými funkcemi zahrnuje také skupinu procesorů; procesor zadaný nebo načtený bezobslužnými funkcemi je relativní vzhledem ke skupině. Podrobnosti najdete v referenčních tématech jednotlivých funkcí.

Aplikace pracující se skupinami může přiřadit všechna svá vlákna konkrétnímu uzlu podobným způsobem, který je popsán výše v tomto tématu, pomocí odpovídajících rozšířených funkcí NUMA. Aplikace používá GetLogicalProcessorInformationEx získat seznam všech procesorů v systému. Aplikace nemůže nastavit masku spřažení procesu, pokud není proces přiřazen k jedné skupině a zamýšlený uzel se nachází v této skupině. Aplikace obvykle musí volat SetThreadGroupAffinity omezit jeho vlákna na zamýšlený uzel.

Chování počínaje windows 10 buildem 20348

Poznámka

Počínaje Windows 10 Build 20348 bylo chování těchto a dalších funkcí NUMA upraveno tak, aby lépe podporovalo systémy s uzly obsahujícími více než 64 procesorů.

Vytvoření "falešných" uzlů pro přizpůsobení mapování 1:1 mezi skupinami a uzly vedlo k matoucímu chování, kdy se hlásí neočekávaná čísla uzlů NUMA, takže počínaje Windows 10 Build 20348 se operační systém změnil tak, aby bylo možné přidružit více skupin k uzlu, takže teď je možné ohlásit skutečnou topologii NUMA systému.

V rámci těchto změn operačního systému se změnilo několik rozhraní API NUMA, která podporují vytváření sestav více skupin, které je teď možné přidružit k jednomu uzlu NUMA. Aktualizovaná a nová rozhraní API jsou označená v tabulce v části rozhraní API NUMA níže.

Vzhledem k tomu, že odebrání rozdělení uzlu může potenciálně ovlivnit existující aplikace, je k dispozici hodnota registru, která umožňuje vyjádřit výslovný souhlas se starším rozdělením uzlů. Rozdělení uzlů je možné znovu povolit vytvořením hodnoty REG_DWORD s názvem SplitLargeNodes s hodnotou 1 pod HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\NUMA. Změny tohoto nastavení vyžadují, aby se projevilo restartování.

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

Poznámka

Aplikace aktualizované tak, aby používaly nové funkce rozhraní API, které hlásí skutečnou topologii NUMA, budou dál fungovat správně v systémech, ve kterých bylo rozdělení velkých uzlů znovu povoleno pomocí tohoto klíče registru.

Následující příklad nejprve ukazuje potenciální problémy s tabulkami mapujícími procesory na uzly NUMA pomocí starších rozhraní API spřažení, které už neposkytují úplný popis všech procesorů v systému, což může vést k neúplné tabulce. Důsledky takové neúplnosti závisí na obsahu tabulky. Pokud tabulka jednoduše uloží odpovídající číslo uzlu, pravděpodobně se jedná pouze o problém s výkonem, kdy v rámci uzlu 0 zůstaly odhalené procesory. Pokud však tabulka obsahuje ukazatele na kontextovou strukturu jednotlivých uzlů, může to vést k dereference null za běhu.

V dalším příkladu kódu jsou znázorněna dvě alternativní řešení problému. Prvním je migrace na rozhraní API pro spřažení uzlů s více skupinami (uživatelský režim a režim jádra). Druhým je použití KeQueryLogicalProcessorRelationship k přímému dotazování uzlu NUMA přidruženého k danému číslu procesoru.


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

Následující tabulka popisuje rozhraní API NUMA.

Funkce Popis
AllocateUserPhysicalPagesNuma Přidělí stránky fyzické paměti, které mají být mapovány a nemapovány v rámci libovolné rozšíření okna adresy (AWE) zadaného procesu a určuje uzel NUMA pro fyzickou paměť.
CreateFileMappingNuma Vytvoří nebo otevře pojmenovaný nebo nepojmenovaný objekt mapování souboru pro zadaný soubor a určí uzel NUMA pro fyzickou paměť.
GetLogicalProcessorInformation Aktualizace ve Windows 10 buildu 20348 Načte informace o logických procesorech a souvisejícím hardwaru.
getLogicalProcessorInformationEx Aktualizace ve Windows 10 buildu 20348 Načte informace o relacích logických procesorů a souvisejícího hardwaru.
GetNumaAvailableMemoryNode Načte množství paměti dostupné v zadaném uzlu.
GetNumaAvailableMemoryNodeEx Načte množství paměti dostupné v uzlu určeném jako hodnota USHORT.
GetNumaHighestNodeNumber Načte uzel, který má aktuálně nejvyšší číslo.
getNumaNodeProcessorMask Aktualizace ve Windows 10 buildu 20348 Načte masku procesoru pro zadaný uzel.
GetNumaNodeProcessorMask2 Novinka ve Windows 10 Build 20348 Načte masku procesoru s více skupinami zadaného uzlu.
GetNumaNodeProcessorMaskEx Aktualizace ve Windows 10 buildu 20348 Načte masku procesoru pro uzel zadaný jako hodnotu USHORT.
GetNumaProcessorNode Načte číslo uzlu pro zadaný procesor.
GetNumaProcessorNodeEx Načte číslo uzlu jako hodnotu USHORT zadaného procesoru.
GetNumaProximityNode Načte číslo uzlu pro zadaný identifikátor bezkontaktní komunikace.
GetNumaProximityNodeEx Načte číslo uzlu jako hodnotu USHORT zadaného identifikátoru bezkontaktní komunikace.
GetProcessDefaultCpuSetMasks Novinka ve Windows 10 Build 20348 Načte seznam sad procesoru ve výchozí sadě procesu, která byla nastavena setProcessDefaultCpuSetMasks nebo SetProcessDefaultCpuSets.
GetThreadSelectedCpuSetMasks Novinka ve Windows 10 Build 20348 Nastaví přiřazení vybraných sad procesoru pro zadané vlákno. Toto přiřazení přepíše výchozí přiřazení procesu, pokud je nastavené.
MapViewOfFileExNuma Mapuje zobrazení mapování souboru do adresního prostoru volajícího procesu a určuje uzel NUMA pro fyzickou paměť.
SetProcessDefaultCpuSetMasks Novinka ve Windows 10 Build 20348 Nastaví výchozí přiřazení sad procesoru pro vlákna v zadaném procesu.
SetThreadSelectedCpuSetMasks Novinka ve Windows 10 Build 20348 Nastaví přiřazení vybraných sad procesoru pro zadané vlákno. Toto přiřazení přepíše výchozí přiřazení procesu, pokud je nastavené.
VirtualAllocExNuma Zarezervuje nebo potvrdí oblast paměti v rámci virtuálního adresního prostoru zadaného procesu a určuje uzel NUMA pro fyzickou paměť.

 

Funkci QueryWorkingSetEx lze použít k načtení uzlu NUMA, na kterém je stránka přidělena. Příklad najdete v tématu Přidělení paměti z uzlu NUMA.

přidělení paměti z uzlu NUMA

více procesorů

skupiny procesorů