NUMA 아키텍처

다중 프로세서 아키텍처의 기존 모델은 대칭 SMP(다중 프로세서)입니다. 이 모델에서 각 프로세서는 메모리 및 I/O에 동일하게 액세스할 수 있습니다. 더 많은 프로세서가 추가되면 프로세서 버스는 시스템 성능에 대한 제한 사항이 됩니다.

시스템 디자이너는 NUMA(비균일 메모리 액세스)를 사용하여 프로세서 버스의 부하를 늘리지 않고 프로세서 속도를 높입니다. 각 프로세서가 메모리의 일부 부분에 가깝고 메모리의 다른 부분과 더 멀리 떨어져 있기 때문에 아키텍처는 균일하지 않습니다. 프로세서는 가까운 메모리에 빠르게 액세스할 수 있지만 멀리 떨어진 메모리에 액세스하는 데 시간이 더 오래 걸릴 수 있습니다.

NUMA 시스템에서 CPU는 노드라는 더 작은 시스템에서 정렬 됩니다. 각 노드에는 자체 프로세서와 메모리가 있으며 캐시 일관성 상호 연결 버스를 통해 더 큰 시스템에 연결됩니다.

시스템은 사용 중인 메모리와 동일한 노드에 있는 프로세서에서 스레드를 예약하여 성능을 향상시키려고 시도합니다. 노드 내에서 메모리 할당 요청을 충족하려고 시도하지만 필요한 경우 다른 노드의 메모리를 할당합니다. 또한 애플리케이션에서 시스템의 토폴로지를 사용할 수 있도록 하는 API를 제공합니다. NUMA 함수를 사용하여 예약 및 메모리 사용을 최적화하여 애플리케이션의 성능을 향상시킬 수 있습니다.

우선, 시스템의 노드 레이아웃을 결정해야 합니다. 시스템에서 번호가 가장 높은 노드를 검색하려면 GetNumaHighestNodeNumber 함수를 사용합니다. 이 숫자는 시스템의 총 노드 수와 동일하도록 보장되지 않습니다. 또한 순차 번호가 있는 노드는 서로 닫히도록 보장되지 않습니다. 시스템에서 프로세서 목록을 검색하려면 GetProcessAffinityMask 함수를 사용합니다. GetNumaProcessorNode 함수를 사용하여 목록의 각 프로세서에 대한 노드를 확인할 수 있습니다. 또는 노드의 모든 프로세서 목록을 검색하려면 GetNumaNodeProcessorMask 함수를 사용합니다.

어떤 프로세서가 어떤 노드에 속하는지 결정한 후에는 애플리케이션의 성능을 최적화할 수 있습니다. 프로세스의 모든 스레드가 동일한 노드에서 실행되도록 하려면 동일한 노드의 프로세서를 지정하는 프로세스 선호도 마스크와 함께 SetProcessAffinityMask 함수를 사용합니다. 이렇게 하면 스레드가 동일한 메모리에 액세스해야 하는 애플리케이션의 효율성이 향상됩니다. 또는 각 노드의 스레드 수를 제한하려면 SetThreadAffinityMask 함수를 사용합니다.

메모리 집약적 애플리케이션은 메모리 사용량을 최적화해야 합니다. 노드에 사용할 수 있는 사용 가능한 메모리 양을 검색하려면 GetNumaAvailableMemoryNode 함수를 사용합니다. VirtualAllocExNuma 함수를 사용하면 애플리케이션에서 메모리 할당에 대한 기본 노드를 지정할 수 있습니다. VirtualAllocExNuma 는 물리적 페이지를 할당하지 않으므로 해당 노드 또는 시스템의 다른 위치에서 페이지를 사용할 수 있는지 여부에 관계없이 성공합니다. 물리적 페이지는 요청 시 할당됩니다. 기본 설정 노드에 페이지가 부족하면 메모리 관리자는 다른 노드의 페이지를 사용합니다. 메모리가 페이징된 경우 메모리를 다시 가져올 때 동일한 프로세스가 사용됩니다.

64개 이상의 논리 프로세서가 있는 시스템에서 NUMA 지원

논리 프로세서가 64개 이상인 시스템에서 노드는 노드의 용량에 따라 프로세서 그룹에 할당됩니다. 노드의 용량은 시스템이 실행되는 동안 추가할 수 있는 추가 논리 프로세서와 함께 시작할 때 존재하는 프로세서의 수입니다.

Windows Server 2008, Windows Vista, Windows Server 2003 및 Windows XP: 프로세서 그룹은 지원되지 않습니다.

각 노드는 그룹 내에 완전히 포함되어야 합니다. 노드의 용량이 상대적으로 작은 경우 시스템은 동일한 그룹에 둘 이상의 노드를 할당하여 성능 향상을 위해 물리적으로 서로 가까운 노드를 선택합니다. 노드의 용량이 그룹의 최대 프로세서 수를 초과하는 경우 시스템은 노드를 여러 개의 더 작은 노드로 분할합니다. 각 노드는 그룹에 들어갈 수 있을 만큼 작습니다.

프로세스를 만들 때 PROC_THREAD_ATTRIBUTE_PREFERRED_NODE 확장 특성을 사용하여 새 프로세스에 적합한 NUMA 노드를 요청할 수 있습니다. 스레드 이상적인 프로세서와 마찬가지로 이상적인 노드는 스케줄러에 대한 힌트이며, 가능한 경우 요청된 노드가 포함된 그룹에 새 프로세스를 할당합니다.

확장된 NUMA 함수 GetNumaAvailableMemoryNodeEx, GetNumaNodeProcessorMaskEx, GetNumaProcessorNodeExGetNumaProximityNodeEx는 노드 번호가 UCHAR가 아닌 USHORT 값이라는 점에서 64개 이상의 논리 프로세서가 있는 시스템에서 잠재적으로 더 많은 수의 노드를 수용할 수 있다는 점에서 의도하지 않은 함수와 다릅니다. 또한 확장 함수에서 지정하거나 검색한 프로세서에는 프로세서 그룹이 포함됩니다. 확장되지 않은 함수에서 지정하거나 검색한 프로세서는 그룹 상대 프로세서입니다. 자세한 내용은 개별 함수 참조 topics 참조하세요.

그룹 인식 애플리케이션은 해당 확장 NUMA 함수를 사용하여 이 항목의 앞부분에서 설명한 것과 비슷한 방식으로 모든 스레드를 특정 노드에 할당할 수 있습니다. 애플리케이션은 GetLogicalProcessorInformationEx 를 사용하여 시스템의 모든 프로세서 목록을 가져옵니다. 프로세스가 단일 그룹에 할당되고 의도한 노드가 해당 그룹에 있지 않으면 애플리케이션에서 프로세스 선호도 마스크를 설정할 수 없습니다. 일반적으로 애플리케이션은 SetThreadGroupAffinity 를 호출하여 스레드를 의도한 노드로 제한해야 합니다.

Windows 10 빌드 20348부터 동작

참고

Windows 10 빌드 20348부터 이 함수 및 기타 NUMA 함수의 동작이 64개 이상의 프로세서를 포함하는 노드가 있는 시스템을 더 잘 지원하도록 수정되었습니다.

그룹과 노드 간의 1:1 매핑을 수용하기 위해 "가짜" 노드를 만들면 예기치 않은 수의 NUMA 노드가 보고되는 혼란스러운 동작이 발생하여 Windows 10 빌드 20348부터 OS가 여러 그룹을 노드와 연결할 수 있도록 변경되었으므로 이제 시스템의 실제 NUMA 토폴로지를 보고할 수 있습니다.

OS에 대한 이러한 변경 내용의 일부로, 이제 단일 NUMA 노드와 연결할 수 있는 여러 그룹 보고를 지원하도록 여러 NUMA API가 변경되었습니다. 업데이트된 API 및 새 API는 아래 NUMA API 섹션의 표에 레이블이 지정됩니다.

노드 분할 제거는 기존 애플리케이션에 잠재적으로 영향을 줄 수 있으므로 레지스트리 값을 사용하여 레거시 노드 분할 동작으로 다시 옵트인할 수 있습니다. HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\NUMA 아래에 값 1이 있는 "SplitLargeNodes"라는 REG_DWORD 값을 만들어 노드 분할을 다시 사용하도록 설정할 수 있습니다. 이 설정을 변경한 후 다시 부팅해야 변경 내용이 적용됩니다.

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

참고

실제 NUMA 토폴로지를 보고하는 새 API 기능을 사용하도록 업데이트된 애플리케이션은 이 레지스트리 키로 큰 노드 분할을 다시 사용하도록 설정된 시스템에서 계속 제대로 작동합니다.

다음 예제에서는 먼저 레거시 선호도 API를 사용하여 프로세서를 NUMA 노드에 매핑하는 빌드 테이블의 잠재적 문제를 보여 줍니다. 이 API는 시스템의 모든 프로세서에 대한 전체 커버를 더 이상 제공하지 않아 불완전한 테이블이 발생할 수 있습니다. 이러한 불완전성의 의미는 테이블의 내용에 따라 달라집니다. 테이블에 해당 노드 번호만 저장하면 발견된 프로세서가 노드 0의 일부로 남아 있는 성능 문제일 수 있습니다. 그러나 테이블에 노드별 컨텍스트 구조에 대한 포인터가 포함되어 있으면 런타임에 NULL 역참조가 발생할 수 있습니다.

다음으로, 코드 예제에서는 문제에 대한 두 가지 해결 방법을 보여 줍니다. 첫 번째는 다중 그룹 노드 선호도 API(사용자 모드 및 커널 모드)로 마이그레이션하는 것입니다. 두 번째는 KeQueryLogicalProcessorRelationship 을 사용하여 지정된 프로세서 번호와 연결된 NUMA 노드를 직접 쿼리하는 것입니다.


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

다음 표에서는 NUMA API에 대해 설명합니다.

함수 Description
AllocateUserPhysicalPagesNuma 지정된 프로세스의 AWE( 주소 창 확장 ) 영역 내에서 매핑 및 매핑 해제할 실제 메모리 페이지를 할당하고 실제 메모리에 대한 NUMA 노드를 지정합니다.
CreateFileMappingNuma 지정된 파일에 대한 명명되거나 명명되지 않은 파일 매핑 개체를 만들거나 열고 실제 메모리에 대한 NUMA 노드를 지정합니다.
GetLogicalProcessorInformation Windows 10 빌드 20348에서 업데이트되었습니다. 논리 프로세서 및 관련 하드웨어에 대한 정보를 검색합니다.
GetLogicalProcessorInformationEx Windows 10 빌드 20348에서 업데이트되었습니다. 논리 프로세서 및 관련 하드웨어의 관계에 대한 정보를 검색합니다.
GetNumaAvailableMemoryNode 지정된 노드에서 사용할 수 있는 메모리 양을 검색합니다.
GetNumaAvailableMemoryNodeEx USHORT 값으로 지정된 노드에서 사용할 수 있는 메모리 양을 검색합니다.
GetNumaHighestNodeNumber 현재 숫자가 가장 높은 노드를 검색합니다.
GetNumaNodeProcessorMask Windows 10 빌드 20348에서 업데이트되었습니다. 지정된 노드의 프로세서 마스크를 검색합니다.
GetNumaNodeProcessorMask2 Windows 10 빌드 20348의 새로운 기능입니다. 지정된 노드의 다중 그룹 프로세서 마스크를 검색합니다.
GetNumaNodeProcessorMaskEx Windows 10 빌드 20348에서 업데이트되었습니다. USHORT 값으로 지정된 노드의 프로세서 마스크를 검색합니다.
GetNumaProcessorNode 지정된 프로세서의 노드 번호를 검색합니다.
GetNumaProcessorNodeEx 노드 번호를 지정된 프로세서에 대한 USHORT 값으로 검색합니다.
GetNumaProximityNode 지정된 근접 식별자의 노드 번호를 검색합니다.
GetNumaProximityNodeEx 노드 번호를 지정된 근접 식별자에 대한 USHORT 값으로 검색합니다.
GetProcessDefaultCpuSetMasks Windows 10 빌드 20348의 새로운 기능 SetProcessDefaultCpuSetMasks 또는 SetProcessDefaultCpuSets로 설정된 프로세스 기본 집합의 CPU 집합 목록을 검색합니다.
GetThreadSelectedCpuSetMasks Windows 10 빌드 20348의 새로운 기능 지정된 스레드에 대해 선택한 CPU 집합 할당을 설정합니다. 이 할당은 프로세스가 설정된 경우 프로세스 기본 할당을 재정의합니다.
MapViewOfFileExNuma 호출 프로세스의 주소 공간에 파일 매핑 보기를 매핑하고 실제 메모리에 대한 NUMA 노드를 지정합니다.
SetProcessDefaultCpuSetMasks Windows 10 빌드 20348의 새로운 기능 지정된 프로세스의 스레드에 대한 기본 CPU 집합 할당을 설정합니다.
SetThreadSelectedCpuSetMasks Windows 10 빌드 20348의 새로운 기능 지정된 스레드에 대해 선택한 CPU 집합 할당을 설정합니다. 이 할당은 프로세스가 설정된 경우 프로세스 기본 할당을 재정의합니다.
VirtualAllocExNuma 지정된 프로세스의 가상 주소 공간 내에서 메모리 영역을 예약하거나 커밋하고 실제 메모리에 대한 NUMA 노드를 지정합니다.

 

QueryWorkingSetEx 함수를 사용하여 페이지가 할당된 NUMA 노드를 검색할 수 있습니다. 예제는 NUMA 노드에서 메모리 할당을 참조하세요.

NUMA 노드에서 메모리 할당

다중 프로세서

프로세서 그룹