Architettura NUMA

Il modello tradizionale per l'architettura multiprocessore è multiprocessore simmetrico (SMP). In questo modello ogni processore ha accesso uguale alla memoria e all'I/O. Man mano che vengono aggiunti più processori, il bus di processore diventa una limitazione per le prestazioni del sistema.

I progettisti di sistema usano l'accesso alla memoria non uniforme (NUMA) per aumentare la velocità del processore senza aumentare il carico sul bus di processore. L'architettura non è uniforme perché ogni processore è vicino a alcune parti della memoria e più lontano da altre parti della memoria. Il processore ottiene rapidamente l'accesso alla memoria vicina, mentre può richiedere più tempo per ottenere l'accesso alla memoria che è più lontano.

In un sistema NUMA le CPU sono disposte in sistemi più piccoli denominati nodi. Ogni nodo ha i propri processori e memoria ed è connesso al sistema più grande tramite un bus di interconnessione coerente con la cache.

Il sistema tenta di migliorare le prestazioni pianificando i thread nei processori che si trovano nello stesso nodo della memoria usata. Tenta di soddisfare le richieste di allocazione della memoria dall'interno del nodo, ma alloca la memoria da altri nodi, se necessario. Fornisce anche un'API per rendere disponibile la topologia del sistema alle applicazioni. È possibile migliorare le prestazioni delle applicazioni usando le funzioni NUMA per ottimizzare l'utilizzo della pianificazione e della memoria.

Prima di tutto, sarà necessario determinare il layout dei nodi nel sistema. Per recuperare il nodo numerato più alto nel sistema, usare la funzione GetNumaHighestNodeNumber . Si noti che questo numero non è garantito uguale al numero totale di nodi nel sistema. Inoltre, i nodi con numeri sequenziali non sono garantiti di essere vicini. Per recuperare l'elenco dei processori nel sistema, usare la funzione GetProcessAffinityMask . È possibile determinare il nodo per ogni processore nell'elenco usando la funzione GetNumaProcessorNode . In alternativa, per recuperare un elenco di tutti i processori in un nodo, usare la funzione GetNumaNodeProcessorMask .

Dopo aver determinato quali processori appartengono ai nodi, è possibile ottimizzare le prestazioni dell'applicazione. Per assicurarsi che tutti i thread per il processo vengano eseguiti nello stesso nodo, usare la funzione SetProcessAffinityMask con una maschera di affinità di processo che specifica i processori nello stesso nodo. Ciò aumenta l'efficienza delle applicazioni i cui thread devono accedere alla stessa memoria. In alternativa, per limitare il numero di thread in ogni nodo, usare la funzione SetThreadAffinityMask .

Le applicazioni a elevato utilizzo della memoria dovranno ottimizzare l'utilizzo della memoria. Per recuperare la quantità di memoria disponibile in un nodo, usare la funzione GetNumaAvailableMemoryNode . La funzione VirtualAllocExNuma consente all'applicazione di specificare un nodo preferito per l'allocazione della memoria. VirtualAllocExNuma non alloca pagine fisiche, quindi avrà esito positivo se le pagine sono disponibili in tale nodo o altrove nel sistema. Le pagine fisiche vengono allocate su richiesta. Se il nodo preferito esce da pagine, gestione memoria userà pagine da altri nodi. Se la memoria viene annullata, lo stesso processo viene usato quando viene restituito.

Supporto NUMA nei sistemi con più di 64 processori logici

Nei sistemi con più di 64 processori logici, i nodi vengono assegnati ai gruppi di processori in base alla capacità dei nodi. La capacità di un nodo è il numero di processori presenti quando il sistema inizia insieme a eventuali processori logici aggiuntivi che possono essere aggiunti durante l'esecuzione del sistema.

Windows Server 2008, Windows Vista, Windows Server 2003 e Windows XP: I gruppi di processori non sono supportati.

Ogni nodo deve essere completamente contenuto all'interno di un gruppo. Se le capacità dei nodi sono relativamente piccole, il sistema assegna più di un nodo allo stesso gruppo, scegliendo i nodi che sono fisicamente vicini tra loro per ottenere prestazioni migliori. Se la capacità di un nodo supera il numero massimo di processori in un gruppo, il sistema suddivide il nodo in più nodi più piccoli, ognuno di essi abbastanza piccolo per adattarsi a un gruppo.

È possibile richiedere un nodo NUMA ideale per un nuovo processo usando l'attributo esteso PROC_THREAD_ATTRIBUTE_PREFERRED_NODE al momento della creazione del processo. Come un processore ideale del thread, il nodo ideale è un hint per l'utilità di pianificazione, che assegna il nuovo processo al gruppo che contiene il nodo richiesto, se possibile.

Le funzioni NUMA estese GetNumaAvailableMemoryNodeEx, GetNumaNodeProcessorMaskEx, GetNumaProcessorNodeEx e GetNumaProximityNodeEx differiscono dalle loro controparti non automatiche in cui il numero di nodo è un valore USHORT anziché un UCHAR, per ospitare il numero potenzialmente maggiore di nodi in un sistema con più di 64 processori logici. Inoltre, il processore specificato con o recuperato dalle funzioni estese include il gruppo di processori; il processore specificato con o recuperato dalle funzioni non automatiche è relativo al gruppo. Per informazioni dettagliate, vedere gli argomenti di riferimento sulle singole funzioni.

Un'applicazione con riconoscimento del gruppo può assegnare tutti i thread a un determinato nodo in modo simile a quello descritto in precedenza in questo argomento, usando le funzioni NUMA estese corrispondenti. L'applicazione usa GetLogicalProcessorInformationEx per ottenere l'elenco di tutti i processori nel sistema. Si noti che l'applicazione non può impostare la maschera di affinità del processo a meno che il processo non venga assegnato a un singolo gruppo e che il nodo previsto si trovi in tale gruppo. In genere l'applicazione deve chiamare SetThreadGroupAffinity per limitare i thread al nodo previsto.

Comportamento a partire da Windows 10 Build 20348

Nota

A partire da Windows 10 Build 20348, il comportamento di questa e altre funzioni NUMA è stato modificato per supportare meglio i sistemi con nodi contenenti più di 64 processori.

La creazione di nodi "fake" per supportare un mapping 1:1 tra gruppi e nodi ha causato comportamenti confusi in cui vengono segnalati numeri imprevisti di nodi NUMA e così, a partire da Windows 10 Build 20348, il sistema operativo è cambiato per consentire a più gruppi di essere associati a un nodo e quindi la vera topologia NUMA del sistema può essere segnalata.

Come parte di queste modifiche al sistema operativo, diverse API NUMA sono state modificate per supportare la creazione di report dei più gruppi che ora possono essere associati a un singolo nodo NUMA. Le API aggiornate e nuove API vengono etichettate nella tabella nella sezione API NUMA seguente.

Poiché la rimozione della suddivisione dei nodi può influire potenzialmente sulle applicazioni esistenti, un valore del Registro di sistema è disponibile per consentire il consenso esplicito nel comportamento di suddivisione del nodo legacy. La suddivisione dei nodi può essere abilitata nuovamente creando un valore REG_DWORD denominato "SplitLargeNodes" con valore 1 sotto HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\NUMA . Le modifiche apportate a questa impostazione richiedono un riavvio per l'effetto.

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

Nota

Le applicazioni aggiornate per l'uso della nuova funzionalità API che segnalano la topologia NUMA true continueranno a funzionare correttamente nei sistemi in cui la suddivisione di nodi di grandi dimensioni è stata riabilitabile con questa chiave del Registro di sistema.

L'esempio seguente illustra prima di tutto i potenziali problemi relativi ai processori di mapping delle tabelle ai nodi NUMA usando le API di affinità legacy, che non forniscono più una copertura completa di tutti i processori nel sistema, ciò può comportare una tabella incompleta. Le implicazioni di tale incompletità dipendono dal contenuto della tabella. Se la tabella archivia semplicemente il numero di nodo corrispondente, questo è probabilmente solo un problema di prestazioni con processori scoperti che vengono lasciati come parte del nodo 0. Tuttavia, se la tabella contiene puntatori a una struttura di contesto per nodo, ciò può comportare dereferenze NULL in fase di esecuzione.

L'esempio di codice illustra quindi due soluzioni alternative per il problema. La prima è eseguire la migrazione alle API di affinità del nodo multi-gruppo (modalità utente e modalità kernel). Il secondo consiste nell'usare KeQueryLogicalProcessorRelationship per eseguire direttamente una query sul nodo NUMA associato a un determinato numero di processore.


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

La tabella seguente descrive l'API NUMA.

Funzione Descrizione
AllocateUserPhysicalPagesNuma Alloca le pagine di memoria fisica da mappare e annullare il mapping all'interno di qualsiasi area AWE ( Address Windowing Extensions ) di un processo specificato e specifica il nodo NUMA per la memoria fisica.
CreateFileMappingNuma Crea o apre un oggetto di mapping di file denominato o senza nome per un file specificato e specifica il nodo NUMA per la memoria fisica.
GetLogicalProcessorInformation Aggiornato in Windows 10 Build 20348. Recupera informazioni sui processori logici e sull'hardware correlato.
GetLogicalProcessorInformationEx Aggiornato in Windows 10 Build 20348. Recupera informazioni sulle relazioni tra processori logici e hardware correlato.
GetNumaAvailableMemoryNode Recupera la quantità di memoria disponibile nel nodo specificato.
GetNumaAvailableMemoryNodeEx Recupera la quantità di memoria disponibile in un nodo specificato come valore USHORT .
GetNumaHighestNodeNumber Recupera il nodo che attualmente ha il numero più alto.
GetNumaNodeProcessorMask Aggiornato in Windows 10 Build 20348. Recupera la maschera del processore per il nodo specificato.
GetNumaNodeProcessorMask2 Novità in Windows 10 Build 20348. Recupera la maschera del processore multi-gruppo del nodo specificato.
GetNumaNodeProcessorMaskEx Aggiornato in Windows 10 Build 20348. Recupera la maschera del processore per un nodo specificato come valore USHORT .
GetNumaProcessorNode Recupera il numero di nodo per il processore specificato.
GetNumaProcessorNodeEx Recupera il numero di nodo come valore USHORT per il processore specificato.
GetNumaProximityNode Recupera il numero di nodo per l'identificatore di prossimità specificato.
GetNumaProximityNodeEx Recupera il numero di nodo come valore USHORT per l'identificatore di prossimità specificato.
GetProcessDefaultCpuSetMasks Novità di Windows 10 Build 20348. Recupera l'elenco dei set di CPU nel set predefinito del processo impostato da SetProcessDefaultCpuSetMasks o SetProcessDefaultCpuSets.
GetThreadSelectedCpuSetMasks Novità di Windows 10 Build 20348. Imposta l'assegnazione dei set di CPU selezionati per il thread specificato. Questa assegnazione sostituisce l'assegnazione predefinita del processo, se impostata.
MapViewOfFileExNuma Esegue il mapping di una visualizzazione di un mapping di file nello spazio indirizzi di un processo chiamante e specifica il nodo NUMA per la memoria fisica.
SetProcessDefaultCpuSetMasks Novità di Windows 10 Build 20348. Imposta l'assegnazione predefinita dei set di CPU per i thread nel processo specificato.
SetThreadSelectedCpuSetMasks Novità di Windows 10 Build 20348. Imposta l'assegnazione dei set di CPU selezionati per il thread specificato. Questa assegnazione sostituisce l'assegnazione predefinita del processo, se impostata.
VirtualAllocExNuma Riserva o esegue il commit di un'area di memoria all'interno dello spazio indirizzi virtuale del processo specificato e specifica il nodo NUMA per la memoria fisica.

 

La funzione QueryWorkingSetEx può essere usata per recuperare il nodo NUMA in cui è allocata una pagina. Per un esempio, vedere Allocazione della memoria da un nodo NUMA.

Allocazione della memoria da un nodo NUMA

Processori multipli

Gruppi di processori