Condividi tramite


Architettura NUMA

Il modello tradizionale per l'architettura multiprocessore è un 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 non uniforme alla memoria (NUMA) per aumentare la velocità del processore senza aumentare il carico sul bus di processore. L'architettura non è uniforme perché ogni processore è vicino ad 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 vengono disposte in sistemi più piccoli denominati nodi . Ogni nodo ha processori e memoria propri 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 in uso. Tenta di soddisfare le richieste di allocazione di memoria dall'interno del nodo, ma allocherà 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, è necessario determinare il layout dei nodi nel sistema. Per recuperare il nodo numerato più alto nel sistema, usare la funzionegetNumaHighestNodeNumber. Si noti che questo numero non è garantito che corrisponda al numero totale di nodi nel sistema. Inoltre, non è garantito che i nodi con numeri sequenziali siano vicini. Per recuperare l'elenco dei processori nel sistema, usare la funzione GetProcessAffinityMask. È possibile determinare il nodo per ogni processore nell'elenco usando la funzioneGetNumaProcessorNode. In alternativa, per recuperare un elenco di tutti i processori in un nodo, usare la funzioneGetNumaNodeProcessorMask.

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 funzionesetProcessAffinityMaskcon una maschera di affinità di processo che specifica i processori nello stesso nodo. In questo modo si 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 funzionesetThreadAffinityMask.

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 funzioneVirtualAllocExNumaconsente all'applicazione di specificare un nodo preferito per l'allocazione di 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 esaurisce le pagine, gestione memoria userà le pagine di altri nodi. Se la memoria viene sottoposta a paging, lo stesso processo viene usato quando viene riportato di nuovo.

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 viene avviato 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 fisicamente vicini l'uno all'altro 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 è sufficientemente piccolo da adattarsi a un gruppo.

È possibile richiedere un nodo NUMA ideale per un nuovo processo usando l'attributo esteso PROC_THREAD_ATTRIBUTE_PREFERRED_NODE quando viene creato il processo. Come un processore ideale del thread, il nodo ideale è un suggerimento 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, GetNumaProcessorNodeExe GetNumaProximityNodeEx differiscono dalle controparti non automatiche in quanto il numero di nodo è un valore USHORT anziché un UCHAR, per contenere il numero di nodi potenzialmente maggiore 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 compatibile con i gruppi 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 sia 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 "falsi" per supportare un mapping 1:1 tra gruppi e nodi ha causato comportamenti confusi in cui vengono segnalati numeri imprevisti di nodi NUMA e quindi, a partire da Windows 10 Build 20348, il sistema operativo è cambiato per consentire l'associazione di più gruppi a un nodo e quindi ora è possibile segnalare la vera topologia NUMA del sistema.

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

Poiché la rimozione della suddivisione dei nodi può influire potenzialmente sulle applicazioni esistenti, è disponibile un valore del Registro di sistema per consentire il consenso esplicito nel comportamento di suddivisione del nodo legacy. La suddivisione dei nodi può essere riabilitata 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 rendere effettivo il riavvio.

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 vera topologia NUMA continueranno a funzionare correttamente nei sistemi in cui la suddivisione di nodi di grandi dimensioni è stata riabilitato con questa chiave del Registro di sistema.

Nell'esempio seguente vengono prima illustrati i potenziali problemi relativi alle tabelle di compilazione che esemplificano i processori 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 incompletezza dipendono dal contenuto della tabella. Se la tabella archivia semplicemente il numero di nodo corrispondente, questo è probabilmente solo un problema di prestazioni con i processori scoperti rimasti come parte del nodo 0. Tuttavia, se la tabella contiene puntatori a una struttura di contesto per nodo, ciò può comportare dereferenziazioni NULL in fase di esecuzione.

L'esempio di codice illustra quindi due soluzioni alternative per il problema. Il primo consiste nel eseguire la migrazione alle API di affinità dei nodi multigruppo (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 di cui eseguire il mapping e il mapping all'interno di qualsiasi area Address Windowing Extensions (AWE) 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 con il numero più alto.
GetNumaNodeProcessorMask Aggiornato in Windows 10 Build 20348. Recupera la maschera del processore per il nodo specificato.
GetNumaNodeProcessorMask2 Novità di Windows 10 Build 20348. Recupera la maschera del processore multigruppo 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 degli 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 viene allocata una pagina. Per un esempio, vedere allocazione della memoria da un nodo NUMA.

allocare memoria da un nodo NUMA

più processori

gruppi di processori