Arquitetura NUMA

O modelo tradicional para arquitetura multiprocessador é SMP (multiprocessador simétrico). Nesse modelo, cada processador tem acesso igual à memória e E/S. À medida que mais processadores são adicionados, o barramento de processador se torna uma limitação para o desempenho do sistema.

Os designers de sistema usam NUMA (acesso à memória não uniforme) para aumentar a velocidade do processador sem aumentar a carga no barramento do processador. A arquitetura não é uniforme porque cada processador está próximo a algumas partes da memória e mais distante de outras partes da memória. O processador obtém rapidamente acesso à memória à qual está próximo, enquanto pode levar mais tempo para obter acesso à memória que está mais distante.

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 está conectado ao sistema maior por meio de um barramento de interconexão coerente de cache.

O sistema tenta melhorar o desempenho agendando threads em processadores que estão no mesmo nó que a memória que está sendo usada. Ele tenta satisfazer 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 disponibilizar a topologia do sistema 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ção GetNumaHighestNodeNumber . Observe que esse número não tem garantia de ser igual ao número total de nós no sistema. Além disso, não há garantia de que nós com números sequenciais estejam próximos. Para recuperar a lista de processadores no sistema, use a função GetProcessAffinityMask . Você pode determinar o nó para cada processador na lista usando a função GetNumaProcessorNode . Como alternativa, para recuperar uma lista de todos os processadores em um nó, use a função GetNumaNodeProcessorMask .

Depois de determinar quais processadores pertencem a quais nós, você pode otimizar o desempenho do aplicativo. Para garantir que todos os threads do processo sejam executados no mesmo nó, use a função SetProcessAffinityMask com uma máscara de afinidade de processo que especifica 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ção SetThreadAffinityMask .

Aplicativos com uso intensivo de memória precisarão otimizar o uso de memória. Para recuperar a quantidade de memória livre disponível para um nó, use a função GetNumaAvailableMemoryNode . A função VirtualAllocExNuma permite que o aplicativo especifique um nó preferencial para a alocação de memória. VirtualAllocExNuma não aloca páginas físicas, portanto, ele terá êxito se as páginas estiverem disponíveis ou não nesse nó ou em outro lugar do sistema. As páginas físicas são alocadas sob demanda. Se o nó preferencial 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 começa junto 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: Não há suporte para grupos de processadores.

Cada nó deve estar totalmente contido em um grupo. Se as capacidades dos nós forem relativamente pequenas, o sistema atribuirá mais de um nó ao mesmo grupo, escolhendo nós fisicamente próximos um do outro para 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 estendido PROC_THREAD_ATTRIBUTE_PREFERRED_NODE 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, GetNumaProcessorNodeEx e GetNumaProximityNodeEx diferem de seus equivalentes não pretendidos, já que o número do nó é um valor 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 pretendidas é 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 começando com Windows 10 Build 20348

Observação

A partir do Windows 10 Build 20348, o comportamento dessa e de outras funções NUMA foi modificado para dar melhor suporte a sistemas com nós que contêm 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 em que números inesperados de nós NUMA são relatados e, portanto, começando com Windows 10 Build 20348, o sistema operacional foi alterado para permitir que vários grupos sejam associados a um nó e, portanto, agora a verdadeira topologia NUMA 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 de nós pode afetar potencialmente os aplicativos existentes, um valor do Registro está disponível para permitir a aceitação de volta ao comportamento de divisão de nó herdado. A divisão de nós pode ser habilitada novamente criando um valor REG_DWORD chamado "SplitLargeNodes" com o valor 1 abaixo de HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\NUMA. As alterações nessa configuração exigem que uma reinicialização entre em vigor.

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

Observação

Os aplicativos atualizados para usar a nova funcionalidade de API que relata a verdadeira topologia NUMA continuarão funcionando corretamente em sistemas em que a divisão de nós grandes foi reenabled com essa chave do Registro.

O exemplo a seguir demonstra primeiro possíveis problemas com tabelas de builds 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, o que pode resultar em uma tabela incompleta. As implicações dessa incompletude dependem do conteúdo da tabela. Se a tabela simplesmente armazenar o número de nó correspondente, isso provavelmente será 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 poderá resultar em desreferências NULL em runtime.

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ó de vários grupos (modo de usuário e modo kernel). A segunda é 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 não mapeadas em qualquer região do AWE ( Extensões de Janela de Endereço ) 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 USHORT .
GetNumaHighestNodeNumber Recupera o nó que atualmente tem o número mais alto.
GetNumaNodeProcessorMask Atualizado no Windows 10 Build 20348. Recupera a máscara de processador para o nó especificado.
GetNumaNodeProcessorMask2 Novo no Windows 10 Build 20348. Recupera a máscara de processador de vários grupos do nó especificado.
GetNumaNodeProcessorMaskEx Atualizado no Windows 10 Build 20348. Recupera a máscara de processador para um nó especificado como um valor USHORT .
GetNumaProcessorNode Recupera o número do nó para o processador especificado.
GetNumaProcessorNodeEx Recupera o número do nó como um valor 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 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 selecionado para o thread especificado. Essa atribuição substituirá a atribuição padrão do processo, se uma estiver definida.
MapViewOfFileExNuma Mapeia uma exibição de um mapeamento de arquivo 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 selecionado para o thread especificado. Essa atribuição substituirá 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ção QueryWorkingSetEx pode ser usada para recuperar o nó NUMA no qual uma página é alocada. Para obter um exemplo, consulte Alocando memória de um nó NUMA.

Alocando memória de um nó NUMA

Vários processadores

Grupos de processadores