Arquitectura NUMA

El modelo tradicional para la arquitectura multiprocesador es un multiprocesador simétrico (SMP). En este modelo, cada procesador tiene el mismo acceso a la memoria y la E/S. A medida que se agregan más procesadores, el bus de procesador se convierte en una limitación para el rendimiento del sistema.

Los diseñadores del sistema usan el acceso a memoria no uniforme (NUMA) para aumentar la velocidad del procesador sin aumentar la carga en el bus del procesador. La arquitectura no es uniforme porque cada procesador está cerca de algunas partes de memoria y más lejos de otras partes de memoria. El procesador obtiene rápidamente acceso a la memoria a la que está cerca, mientras que puede tardar más tiempo en obtener acceso a la memoria que está más lejos.

En un sistema NUMA, las CPU se organizan en sistemas más pequeños denominados nodos. Cada nodo tiene sus propios procesadores y memoria, y está conectado al sistema mayor a través de un bus de interconexión coherente con la memoria caché.

El sistema intenta mejorar el rendimiento mediante la programación de subprocesos en procesadores que están en el mismo nodo que la memoria que se usa. Intenta satisfacer las solicitudes de asignación de memoria desde dentro del nodo, pero asignará memoria de otros nodos si es necesario. También proporciona una API para que la topología del sistema esté disponible para las aplicaciones. Puede mejorar el rendimiento de las aplicaciones mediante las funciones NUMA para optimizar la programación y el uso de memoria.

En primer lugar, deberá determinar el diseño de los nodos en el sistema. Para recuperar el nodo numerado más alto del sistema, use la función GetNumaHighestNodeNumber . Tenga en cuenta que no se garantiza que este número sea igual al número total de nodos del sistema. Además, no se garantiza que los nodos con números secuenciales estén cerca. Para recuperar la lista de procesadores del sistema, use la función GetProcessAffinityMask . Puede determinar el nodo de cada procesador de la lista mediante la función GetNumaProcessorNode . Como alternativa, para recuperar una lista de todos los procesadores de un nodo, use la función GetNumaNodeProcessorMask .

Después de determinar qué procesadores pertenecen a los nodos, puede optimizar el rendimiento de la aplicación. Para asegurarse de que todos los subprocesos del proceso se ejecutan en el mismo nodo, use la función SetProcessAffinityMask con una máscara de afinidad de proceso que especifique procesadores en el mismo nodo. Esto aumenta la eficacia de las aplicaciones cuyos subprocesos necesitan tener acceso a la misma memoria. Como alternativa, para limitar el número de subprocesos en cada nodo, use la función SetThreadAffinityMask .

Las aplicaciones que consumen mucha memoria tendrán que optimizar su uso de memoria. Para recuperar la cantidad de memoria libre disponible para un nodo, use la función GetNumaAvailableMemoryNode . La función VirtualAllocExNuma permite a la aplicación especificar un nodo preferido para la asignación de memoria. VirtualAllocExNuma no asigna ninguna página física, por lo que se realizará correctamente si las páginas están disponibles en ese nodo o en otro lugar del sistema. Las páginas físicas se asignan a petición. Si el nodo preferido se queda sin páginas, el administrador de memoria usará páginas de otros nodos. Si la memoria se pagina, se usa el mismo proceso cuando se devuelve.

Compatibilidad con NUMA en sistemas con más de 64 procesadores lógicos

En sistemas con más de 64 procesadores lógicos, los nodos se asignan a grupos de procesadores según la capacidad de los nodos. La capacidad de un nodo es el número de procesadores que están presentes cuando el sistema se inicia junto con los procesadores lógicos adicionales que se pueden agregar mientras se ejecuta el sistema.

Windows Server 2008, Windows Vista, Windows Server 2003 y Windows XP: No se admiten grupos de procesadores.

Cada nodo debe estar totalmente contenido dentro de un grupo. Si las capacidades de los nodos son relativamente pequeñas, el sistema asigna más de un nodo al mismo grupo, eligiendo nodos que están físicamente cerca entre sí para mejorar el rendimiento. Si la capacidad de un nodo supera el número máximo de procesadores de un grupo, el sistema divide el nodo en varios nodos más pequeños, cada uno lo suficientemente pequeño como para caber en un grupo.

Se puede solicitar un nodo NUMA ideal para un nuevo proceso mediante el atributo extendido PROC_THREAD_ATTRIBUTE_PREFERRED_NODE cuando se crea el proceso. Al igual que un procesador ideal para subprocesos, el nodo ideal es una sugerencia para el programador, que asigna el nuevo proceso al grupo que contiene el nodo solicitado si es posible.

Las funciones NUMA extendidas GetNumaAvailableMemoryNodeEx, GetNumaNodeProcessorMaskEx, GetNumaProcessorNodeEx y GetNumaProximityNodeEx difieren de sus homólogos no extendidos en que el número de nodo es un valor de USHORT en lugar de un UCHAR, para dar cabida al número de nodos potencialmente mayor en un sistema con más de 64 procesadores lógicos. Además, el procesador especificado con o recuperado por las funciones extendidas incluye el grupo de procesadores; el procesador especificado con o recuperado por las funciones sin espacio es relativo al grupo. Para obtener más información, consulte los temas de referencia de funciones individuales.

Una aplicación compatible con grupos puede asignar todos sus subprocesos a un nodo determinado de forma similar a la descrita anteriormente en este tema, mediante las funciones NUMA extendidas correspondientes. La aplicación usa GetLogicalProcessorInformationEx para obtener la lista de todos los procesadores del sistema. Tenga en cuenta que la aplicación no puede establecer la máscara de afinidad de proceso a menos que el proceso se asigne a un único grupo y el nodo previsto se encuentre en ese grupo. Normalmente, la aplicación debe llamar a SetThreadGroupAffinity para limitar sus subprocesos al nodo previsto.

Comportamiento a partir de Windows 10 compilación 20348

Nota

A partir de Windows 10 compilación 20348, el comportamiento de esta y otras funciones NUMA se ha modificado para admitir mejor los sistemas con nodos que contienen más de 64 procesadores.

La creación de nodos "falsos" para dar cabida a una asignación de 1:1 entre grupos y nodos ha dado lugar a comportamientos confusos en los que se notifican números inesperados de nodos NUMA, por lo que, a partir de Windows 10 compilación 20348, el sistema operativo ha cambiado para permitir que varios grupos se asocien a un nodo, por lo que ahora se puede notificar la topología NUMA verdadera del sistema.

Como parte de estos cambios en el sistema operativo, varias API de NUMA han cambiado para admitir la creación de informes de varios grupos que ahora se pueden asociar a un único nodo NUMA. Las API nuevas y actualizadas se etiquetan en la tabla de la sección API de NUMA siguiente.

Dado que la eliminación de la división de nodos puede afectar potencialmente a las aplicaciones existentes, hay disponible un valor del Registro para permitir volver a participar en el comportamiento de división de nodos heredado. La división de nodos se puede volver a habilitar mediante la creación de un valor de REG_DWORD denominado "SplitLargeNodes" con el valor 1 debajo de HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\NUMA. Los cambios en esta configuración requieren un reinicio para surtir efecto.

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

Nota

Las aplicaciones que se actualizan para usar la nueva funcionalidad de API que informa de la topología NUMA verdadera seguirán funcionando correctamente en sistemas donde se ha vuelto a habilitar la división de nodos grandes con esta clave del Registro.

En el ejemplo siguiente se muestran primero posibles problemas con compilaciones de procesadores de asignación de tablas a nodos NUMA mediante las API de afinidad heredadas, que ya no proporcionan una cobertura completa de todos los procesadores del sistema, lo que puede dar lugar a una tabla incompleta. Las implicaciones de tal integridad dependen del contenido de la tabla. Si la tabla simplemente almacena el número de nodo correspondiente, es probable que solo se deje un problema de rendimiento con procesadores descubiertos como parte del nodo 0. Sin embargo, si la tabla contiene punteros a una estructura de contexto por nodo, esto puede dar lugar a desreferencias NULL en tiempo de ejecución.

A continuación, el ejemplo de código muestra dos soluciones alternativas para el problema. La primera consiste en migrar a las API de afinidad de nodo de varios grupos (modo de usuario y modo kernel). El segundo consiste en usar KeQueryLogicalProcessorRelationship para consultar directamente el nodo NUMA asociado a un número de procesador determinado.


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

En la tabla siguiente se describe la API NUMA.

Función Descripción
AllocateUserPhysicalPagesNuma Asigna páginas de memoria física que se van a asignar y desasignar dentro de cualquier región de Extensiones de ventanas de direcciones (AWE) de un proceso especificado y especifica el nodo NUMA para la memoria física.
CreateFileMappingNuma Crea o abre un objeto de asignación de archivos con nombre o sin nombre para un archivo especificado y especifica el nodo NUMA para la memoria física.
GetLogicalProcessorInformation Actualizado en Windows 10 compilación 20348. Recupera información sobre procesadores lógicos y hardware relacionado.
GetLogicalProcessorInformationEx Actualizado en Windows 10 compilación 20348. Recupera información sobre las relaciones de los procesadores lógicos y el hardware relacionado.
GetNumaAvailableMemoryNode Recupera la cantidad de memoria disponible en el nodo especificado.
GetNumaAvailableMemoryNodeEx Recupera la cantidad de memoria disponible en un nodo especificado como un valor de USHORT .
GetNumaHighestNodeNumber Recupera el nodo que actualmente tiene el número más alto.
GetNumaNodeProcessorMask Actualizado en Windows 10 compilación 20348. Recupera la máscara del procesador para el nodo especificado.
GetNumaNodeProcessorMask2 Novedad de Windows 10 compilación 20348. Recupera la máscara de procesador de varios grupos del nodo especificado.
GetNumaNodeProcessorMaskEx Actualizado en Windows 10 compilación 20348. Recupera la máscara de procesador de un nodo especificado como un valor de USHORT .
GetNumaProcessorNode Recupera el número de nodo del procesador especificado.
GetNumaProcessorNodeEx Recupera el número de nodo como un valor de USHORT para el procesador especificado.
GetNumaProximityNode Recupera el número de nodo del identificador de proximidad especificado.
GetNumaProximityNodeEx Recupera el número de nodo como un valor de USHORT para el identificador de proximidad especificado.
GetProcessDefaultCpuSetMasks Novedades de Windows 10 compilación 20348. Recupera la lista de conjuntos de CPU del conjunto predeterminado del proceso establecido por SetProcessDefaultCpuSetMasks o SetProcessDefaultCpuSets.
GetThreadSelectedCpuSetMasks Novedades de Windows 10 compilación 20348. Establece la asignación de conjuntos de CPU seleccionados para el subproceso especificado. Esta asignación invalida la asignación predeterminada del proceso, si se establece una.
MapViewOfFileExNuma Asigna una vista de una asignación de archivos al espacio de direcciones de un proceso de llamada y especifica el nodo NUMA para la memoria física.
SetProcessDefaultCpuSetMasks Novedades de Windows 10 compilación 20348. Establece la asignación predeterminada de conjuntos de CPU para subprocesos en el proceso especificado.
SetThreadSelectedCpuSetMasks Novedades de Windows 10 compilación 20348. Establece la asignación de conjuntos de CPU seleccionados para el subproceso especificado. Esta asignación invalida la asignación predeterminada del proceso, si se establece una.
VirtualAllocExNuma Reserva o confirma una región de memoria dentro del espacio de direcciones virtuales del proceso especificado y especifica el nodo NUMA para la memoria física.

 

La función QueryWorkingSetEx se puede usar para recuperar el nodo NUMA en el que se asigna una página. Para obtener un ejemplo, vea Asignar memoria desde un nodo NUMA.

Asignar memoria desde un nodo NUMA

Varios procesadores

Grupos de procesadores