Arquitectura NUMA
El modelo tradicional para la arquitectura multiprocesador es 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 más grande 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ónGetNumaHighestNodeNumber. 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 de 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ónVirtualAllocExNuma permite a la aplicación especificar un nodo preferido para la asignación de memoria. VirtualAllocExNuma no asigna páginas físicas, por lo que tendrá éxito 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 los 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 cualquier procesador lógico adicional que se pueda agregar mientras se ejecuta el sistema.
Windows Server 2008, Windows Vista, Windows Server 2003 y Windows XP: no se admiten grupos de procesadores de.
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, GetNumaProcessorNodeExy GetNumaProximityNodeEx difieren de sus homólogos no usados en que el número de nodo es un valor de USHORT en lugar de un UCHAR, para acomodar el número potencialmente mayor de nodos 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 noextendidas 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 la compilación 20348 de Windows 10
Nota
A partir de la compilación 20348 de Windows 10, 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 provocado comportamientos confusos en los que se notifican números inesperados de nodos NUMA, por lo que, a partir de la compilación 20348 de Windows 10, el sistema operativo ha cambiado para permitir que varios grupos estén asociados a un nodo y, por tanto, ahora se puede notificar la verdadera topología NUMA del sistema.
Como parte de estos cambios en el sistema operativo, una serie de API 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 actualizadas y nuevas se etiquetan en la tabla en 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 que surta 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 en los que 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 las 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ísicas que se asignarán y desasignarán dentro de cualquier extensiones de ventana de direcciones (AWE) región 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 la compilación 20348 de Windows 10. Recupera información sobre procesadores lógicos y hardware relacionado. |
GetLogicalProcessorInformationEx | Actualizado en la compilación 20348 de Windows 10. 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 la compilación 20348 de Windows 10. Recupera la máscara de procesador para el nodo especificado. |
GetNumaNodeProcessorMask2 | Novedades de la compilación 20348 de Windows 10. Recupera la máscara de procesador de varios grupos del nodo especificado. |
GetNumaNodeProcessorMaskEx | Actualizado en la compilación 20348 de Windows 10. Recupera la máscara de procesador de un nodo especificado como un valor de USHORT. |
GetNumaProcessorNode de | Recupera el número de nodo del procesador especificado. |
GetNumaProcessorNodeEx | Recupera el número de nodo como un 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 valor de de USHORT para el identificador de proximidad especificado. |
GetProcessDefaultCpuSetMasks | Novedades de la compilación 20348 de Windows 10. Recupera la lista de conjuntos de CPU en el conjunto predeterminado del proceso establecido por SetProcessDefaultCpuSetMasks o SetProcessDefaultCpuSets. |
GetThreadSelectedCpuSetMasks | Novedades de la compilación 20348 de Windows 10. 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 en el espacio de direcciones de un proceso de llamada y especifica el nodo NUMA para la memoria física. |
SetProcessDefaultCpuSetMasks | Novedades de la compilación 20348 de Windows 10. Establece la asignación predeterminada de conjuntos de CPU para subprocesos en el proceso especificado. |
setThreadSelectedCpuSetMasks | Novedades de la compilación 20348 de Windows 10. 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 virtual 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.
Temas relacionados