Arquitetura NUMA
O modelo tradicional para arquitetura multiprocessador é o multiprocessador simétrico (SMP). Neste modelo, cada processador tem igual acesso à memória e E/S. À medida que mais processadores são adicionados, o barramento do processador se torna uma limitação para o desempenho do sistema.
Os projetistas de sistemas usam o acesso não uniforme à memória (NUMA) para aumentar a velocidade do processador sem aumentar a carga no barramento do processador. A arquitetura não é uniforme porque cada processador está perto de algumas partes da memória e mais longe de outras partes da memória. O processador rapidamente ganha acesso à memória que está perto, enquanto pode levar mais tempo para obter acesso à memória que está mais longe.
Em um sistema NUMA, as CPUs são organizadas em sistemas menores chamados nós . Cada nó tem seus próprios processadores e memória, e é conectado ao sistema maior através de um barramento de interconexão coerente com cache.
O sistema tenta melhorar o desempenho agendando threads em processadores que estão no mesmo nó da memória que está sendo usada. Ele tenta satisfazer as solicitações de alocação de memória de dentro do nó, mas alocará memória de outros nós, se necessário. Ele também fornece uma API para tornar a topologia do sistema disponível para aplicativos. Você pode melhorar o desempenho de seus aplicativos usando as funções NUMA para otimizar o agendamento e o uso de memória.
Em primeiro lugar, você precisará determinar o layout dos nós no sistema. Para recuperar o nó numerado mais alto do sistema, use a funçãoGetNumaHighestNodeNumber. Observe que esse número não é garantido para igualar o número total de nós no sistema. Além disso, não é garantido que os nós com números sequenciais estejam próximos. Para recuperar a lista de processadores no sistema, use o função GetProcessAffinityMask. Você pode determinar o nó para cada processador na lista usando a funçãoGetNumaProcessorNode. Como alternativa, para recuperar uma lista de todos os processadores em um nó, use a funçãoGetNumaNodeProcessorMask.
Depois de determinar quais processadores pertencem a quais nós, você pode otimizar o desempenho do seu aplicativo. Para garantir que todos os threads do seu processo sejam executados no mesmo nó, use a funçãoSetProcessAffinityMask com uma máscara de afinidade de processo que especifique processadores no mesmo nó. Isso aumenta a eficiência de aplicativos cujos threads precisam acessar a mesma memória. Como alternativa, para limitar o número de threads em cada nó, use a funçãoSetThreadAffinityMask.
Aplicativos que consomem muita memória precisarão otimizar seu uso de memória. Para recuperar a quantidade de memória livre disponível para um nó, use a funçãoGetNumaAvailableMemoryNode. A funçãoVirtualAllocExNuma permite que o aplicativo especifique um nó preferencial para a alocação de memória. VirtualAllocExNuma não aloca nenhuma página física, portanto, ele terá sucesso se as páginas estiverem disponíveis nesse nó ou em outro lugar do sistema. As páginas físicas são alocadas sob demanda. Se o nó preferido ficar sem páginas, o gerenciador de memória usará páginas de outros nós. Se a memória for paginada, o mesmo processo será usado quando ela for trazida de volta.
Suporte NUMA em sistemas com mais de 64 processadores lógicos
Em sistemas com mais de 64 processadores lógicos, os nós são atribuídos a grupos de processadores de acordo com a capacidade dos nós. A capacidade de um nó é o número de processadores que estão presentes quando o sistema é iniciado juntamente com quaisquer processadores lógicos adicionais que podem ser adicionados enquanto o sistema está em execução.
Windows Server 2008, Windows Vista, Windows Server 2003 e Windows XP: grupos de processadores não são suportados.
Cada nó deve estar totalmente contido dentro de um grupo. Se as capacidades dos nós forem relativamente pequenas, o sistema atribui mais de um nó ao mesmo grupo, escolhendo nós que estão fisicamente próximos uns dos outros para um melhor desempenho. Se a capacidade de um nó exceder o número máximo de processadores em um grupo, o sistema dividirá o nó em vários nós menores, cada um pequeno o suficiente para caber em um grupo.
Um nó NUMA ideal para um novo processo pode ser solicitado usando o atributo PROC_THREAD_ATTRIBUTE_PREFERRED_NODE estendido quando o processo é criado. Como um processador ideal de thread, o nó ideal é uma dica para o agendador, que atribui o novo processo ao grupo que contém o nó solicitado, se possível.
As funções NUMA estendidas GetNumaAvailableMemoryNodeEx, GetNumaNodeProcessorMaskEx, GetNumaProcessorNodeExe GetNumaProximityNodeEx diferem de suas contrapartes não estendidas porque o número do nó é um valor de USHORT em vez de um UCHAR, para acomodar o número potencialmente maior de nós em um sistema com mais de 64 processadores lógicos. Além disso, o processador especificado com ou recuperado pelas funções estendidas inclui o grupo de processadores; O processador especificado com ou recuperado pelas funções não estendidas é relativo ao grupo. Para obter detalhes, consulte os tópicos de referência de função individual.
Um aplicativo com reconhecimento de grupo pode atribuir todos os seus threads a um nó específico de maneira semelhante à descrita anteriormente neste tópico, usando as funções NUMA estendidas correspondentes. O aplicativo usa GetLogicalProcessorInformationEx para obter a lista de todos os processadores no sistema. Observe que o aplicativo não pode definir a máscara de afinidade de processo, a menos que o processo seja atribuído a um único grupo e o nó pretendido esteja localizado nesse grupo. Normalmente, o aplicativo deve chamar SetThreadGroupAffinity para limitar seus threads ao nó pretendido.
Comportamento a partir do Windows 10 Build 20348
Observação
A partir do Windows 10 Build 20348, o comportamento desta e de outras funções NUMA foi modificado para suportar melhor sistemas com nós contendo mais de 64 processadores.
A criação de nós "falsos" para acomodar um mapeamento 1:1 entre grupos e nós resultou em comportamentos confusos onde números inesperados de nós NUMA são relatados e, portanto, a partir do Windows 10 Build 20348, o sistema operacional mudou para permitir que vários grupos sejam associados a um nó, e agora a topologia NUMA verdadeira do sistema pode ser relatada.
Como parte dessas alterações no sistema operacional, várias APIs NUMA foram alteradas para dar suporte ao relatório de vários grupos que agora podem ser associados a um único nó NUMA. As APIs atualizadas e novas são rotuladas na tabela na seção API NUMA abaixo.
Como a remoção da divisão do nó pode potencialmente afetar os aplicativos existentes, um valor do Registro está disponível para permitir a opção de volta ao comportamento de divisão do nó herdado. A divisão de nós pode ser reativada criando um valor REG_DWORD chamado "SplitLargeNodes" com o valor 1 abaixo HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\NUMA. As alterações nessa configuração exigem uma reinicialização para entrar em vigor.
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\NUMA" /v SplitLargeNodes /t REG_DWORD /d 1
Observação
Os aplicativos que são atualizados para usar a nova funcionalidade de API que relata a topologia NUMA verdadeira continuarão a funcionar corretamente em sistemas onde a divisão de nós grandes foi reativada com essa chave do Registro.
O exemplo a seguir demonstra primeiro possíveis problemas com tabelas de compilações mapeando processadores para nós NUMA usando as APIs de afinidade herdadas, que não fornecem mais uma cobertura completa de todos os processadores no sistema, isso pode resultar em uma tabela incompleta. As implicações de tal incompletude dependem do conteúdo da tabela. Se a tabela simplesmente armazena o número do nó correspondente, isso provavelmente é apenas um problema de desempenho com processadores descobertos sendo deixados como parte do nó 0. No entanto, se a tabela contiver ponteiros para uma estrutura de contexto por nó, isso pode resultar em desreferências NULL em tempo de execução.
Em seguida, o exemplo de código ilustra duas soluções alternativas para o problema. A primeira é migrar para as APIs de afinidade de nó multigrupo (modo de usuário e modo kernel). O segundo é usar KeQueryLogicalProcessorRelationship para consultar diretamente o nó NUMA associado a um determinado número de processador.
//
// 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
A tabela a seguir descreve a API NUMA.
Função | Descrição |
---|---|
AllocateUserPhysicalPagesNuma | Aloca páginas de memória física a serem mapeadas e desmapeadas dentro de qualquer região AWE (Address Windowing Extensions) de um processo especificado e especifica o nó NUMA para a memória física. |
CreateFileMappingNuma | Cria ou abre um objeto de mapeamento de arquivo nomeado ou sem nome para um arquivo especificado e especifica o nó NUMA para a memória física. |
GetLogicalProcessorInformation | Atualizado no Windows 10 Build 20348. Recupera informações sobre processadores lógicos e hardware relacionado. |
GetLogicalProcessorInformationEx | Atualizado no Windows 10 Build 20348. Recupera informações sobre as relações de processadores lógicos e hardware relacionado. |
GetNumaAvailableMemoryNode | Recupera a quantidade de memória disponível no nó especificado. |
GetNumaAvailableMemoryNodeEx | Recupera a quantidade de memória disponível em um nó especificado como um valor de USHORT. |
GetNumaHighestNodeNumber | Recupera o nó que atualmente tem o número mais alto. |
GetNumaNodeProcessorMask | Atualizado no Windows 10 Build 20348. Recupera a máscara do processador para o nó especificado. |
GetNumaNodeProcessorMask2 | Novo no Windows 10 Build 20348. Recupera a máscara de processador multigrupo do nó especificado. |
GetNumaNodeProcessorMaskEx | Atualizado no Windows 10 Build 20348. Recupera a máscara do processador para um nó especificado como um valor de USHORT. |
GetNumaProcessorNode | Recupera o número do nó para o processador especificado. |
GetNumaProcessorNodeEx | Recupera o número do nó como um valor de USHORT para o processador especificado. |
GetNumaProximityNode | Recupera o número do nó para o identificador de proximidade especificado. |
GetNumaProximityNodeEx | Recupera o número do nó como um valor de USHORT para o identificador de proximidade especificado. |
GetProcessDefaultCpuSetMasks | Novo no Windows 10 Build 20348. Recupera a lista de Conjuntos de CPU no conjunto padrão do processo que foi definido por SetProcessDefaultCpuSetMasks ou SetProcessDefaultCpuSets. |
GetThreadSelectedCpuSetMasks | Novo no Windows 10 Build 20348. Define a atribuição de Conjuntos de CPU selecionada para o thread especificado. Esta atribuição substitui a atribuição padrão do processo, se uma estiver definida. |
MapViewOfFileExNuma | Mapeia uma exibição de um arquivo mapeado para o espaço de endereço de um processo de chamada e especifica o nó NUMA para a memória física. |
SetProcessDefaultCpuSetMasks | Novo no Windows 10 Build 20348. Define a atribuição padrão de Conjuntos de CPU para threads no processo especificado. |
SetThreadSelectedCpuSetMasks | Novo no Windows 10 Build 20348. Define a atribuição de Conjuntos de CPU selecionada para o thread especificado. Esta atribuição substitui a atribuição padrão do processo, se uma estiver definida. |
VirtualAllocExNuma | Reserva ou confirma uma região de memória dentro do espaço de endereço virtual do processo especificado e especifica o nó NUMA para a memória física. |
A funçãoQueryWorkingSetEx pode ser usada para recuperar o nó NUMA no qual uma página está alocada. Para obter um exemplo, consulte Allocating Memory from a NUMA Node.
Tópicos relacionados