Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1162. C# - 인텔 CPU의 P-Core와 E-Core를 구분하는 방법 [링크 복사], [링크+제목 복사],
조회: 10435
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 3개 있습니다.)

C# - 인텔 CPU의 P-Core와 E-Core를 구분하는 방법

작년 말에 구매한 PC의 경우 엘더레이크 CPU를 장착하고 있는데요,

인텔 코어i9-12세대 12900K (엘더레이크) 정품
; http://prod.danawa.com/info/?pcode=15594887&cate=11341237

이 제품의 소개를 보면,

코어 수: 8+8 코어
스레드 수: 16+8 스레드

이런 식으로 표기가 되어 있습니다. 의미인즉, 8개의 P-Core와 8개의 E-Core로 나뉜다는 것인데, P-Core는 제 성능을 발휘할 수 있는 데다 Hyper-Threading도 지원을 하고 있어 8개의 P-Core가 16개의 스레드 수를 갖는 것이고, 반면 E-Core는 시스템의 작업 부하가 낮을 때 선택돼 저전력으로 동작하는 것으로 8개의 E-Core가 하이퍼스레딩 없이 각각 1개의 스레드를 담당할 수 있습니다.

이로 인해, 만약 개발자가 특정 스레드의 성능을 높이기 위해 Thread-affinity를 부여하고 싶다면 대상 코어가 P-Coer인지, E-Core인지 확인해야 할 필요가 생긴 것입니다. 관련해서는 이미 인텔에서 자세한 자료를 배포하고 있는데요,

Game Dev Guide for Alder Lake Performance Hybrid Architecture
; https://www.intel.com/content/www/us/en/developer/articles/guide/alder-lake-developer-guide.html

그래서 Win32 API에도 이를 위한 정보를 구하려면 GetSystemCpuSetInformation 함수를 이용하면 됩니다.

GetSystemCpuSetInformation function
; https://learn.microsoft.com/en-us/windows/win32/procthread/getsystemcpusetinformation

SYSTEM_CPU_SET_INFORMATION structure (winnt.h)
; https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-system_cpu_set_information

간단하게 C#으로 구현해 볼까요? ^^ 전체 소스 코드는 다음과 같습니다.

using System.Collections;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace Console1
{
    internal class NativeMethods
    {
        [DllImport("kernel32.dll")]
        internal static extern uint GetCurrentThreadId();

        [DllImport("kernel32.dll", SetLastError = true, EntryPoint = "GetSystemCpuSetInformation")]
        static extern unsafe bool _GetSystemCpuSetInformation(byte* Information, uint BufferLength,
            out uint ReturnedLength, IntPtr Process, uint Flags);

        public static CpuInfo GetSystemCpuSetInformation()
        {
            IntPtr currentProcess = Process.GetCurrentProcess().Handle;
            return GetSystemCpuSetInformation(currentProcess);
        }

        public static unsafe CpuInfo GetSystemCpuSetInformation(IntPtr processHandle)
        {
            List<SYSTEM_CPU_SET_INFORMATION> list = new List<SYSTEM_CPU_SET_INFORMATION>();

            uint size;

            do
            {
                bool result = NativeMethods.GetSystemCpuSetInformationRequiredSize(processHandle, out size);
                if (result == false)
                {
                    break;
                }

                byte[] buffer = new byte[size];

                fixed (byte* pBuffer = buffer)
                {
                    result = _GetSystemCpuSetInformation(pBuffer, size, out _, processHandle, 0);
                    if (result == false)
                    {
                        break;
                    }

                    SYSTEM_CPU_SET_INFORMATION* pItem = (SYSTEM_CPU_SET_INFORMATION*)pBuffer;
                    int itemSize = sizeof(SYSTEM_CPU_SET_INFORMATION);

                    if ((size % itemSize) != 0)
                    {
                        break;
                    }

                    uint loopCOunt = size / (uint)itemSize;

                    for (int i = 0; i < loopCOunt; i++)
                    {
                        list.Add(*pItem);
                        pItem++;
                    }
                }
            } while (false);

            return new CpuInfo(list);
        }

        static unsafe bool GetSystemCpuSetInformationRequiredSize(IntPtr processHandle, out uint size)
        {
            NativeMethods._GetSystemCpuSetInformation(null, 0, out size, processHandle, 0);

            uint lastError = NativeMethods.GetLastError();
            if (lastError == (uint)Win32Error.ERROR_INSUFFICIENT_BUFFER)
            {
                return true;
            }

            return false;
        }

        [DllImport("kernel32.dll")]
        public static extern uint GetLastError();
    }

    public enum Win32Error
    {
        // MessageId: ERROR_INSUFFICIENT_BUFFER
        // MessageText:
        // The data area passed to a system call is too small.
        ERROR_INSUFFICIENT_BUFFER = 122,
    }

    public enum CPU_SET_INFORMATION_TYPE
    {
        CpuSetInformation
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct CPU_INNER_STATUS
    {
        public byte Status;

        public bool Parked
        {
            get { return (Status & (int)CpuStatusBit.Parked) == 1; }
        }

        public bool Allocated
        {
            get { return (Status & (int)CpuStatusBit.Allocated) == 1; }
        }

        public bool AllocatedToTargetProcess
        {
            get { return (Status & (int)CpuStatusBit.AllocatedToTargetProcess) == 1; }
        }

        public bool RealTime
        {
            get { return (Status & (int)CpuStatusBit.RealTime) == 1; }
        }

        [Flags]
        enum CpuStatusBit
        {
            Parked = 0x01,
            Allocated = 0x02,
            AllocatedToTargetProcess = 0x04,
            RealTime = 0x08,
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct CPU_STATUS
    {
        public byte AllFlags;
        public CPU_INNER_STATUS CpuStatus;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct CPU_CLASS
    {
        public uint Reserved;
        public byte SchedulingClass;
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct CPU_SET
    {
        [FieldOffset(0)]
        public uint Id;
        [FieldOffset(4)]
        public short Group;
        [FieldOffset(6)]
        public byte LogicalProcessorIndex;
        [FieldOffset(7)]
        public byte CoreIndex;
        [FieldOffset(8)]
        public byte LastLevelCacheIndex;
        [FieldOffset(9)]
        public byte NumaNodeIndex;
        [FieldOffset(10)]
        public byte EfficiencyClass;

        [FieldOffset(11)]
        public CPU_STATUS FlagsAndStatus;

        [FieldOffset(11)]
        public CPU_CLASS Scheduling;

        [FieldOffset(16)]
        public ulong AllocationTag;
    }

    public class CpuInfo : IEnumerable<SYSTEM_CPU_SET_INFORMATION>
    {
        readonly List<SYSTEM_CPU_SET_INFORMATION> _list;
        readonly bool _isHybrid;
        readonly int _pcoreCount;
        readonly int _ecoreCount;

        internal CpuInfo(List<SYSTEM_CPU_SET_INFORMATION> list)
        {
            _list = list;

            _pcoreCount = _list.Count((e) => e.IsPCore == true);
            _ecoreCount = _list.Count((e) => e.IsECore == true);

            _isHybrid = _pcoreCount > 0 && _ecoreCount > 0;

            if (_isHybrid == false)
            {
                _pcoreCount = 0;
                _ecoreCount = 0;
            }
        }

        public int LogicalCoreCount => _list.Count;

        public SYSTEM_CPU_SET_INFORMATION this[int index] => _list[index];

        public IEnumerator<SYSTEM_CPU_SET_INFORMATION> GetEnumerator() => _list.GetEnumerator();

        IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator();

        public bool IsHybrid => _isHybrid;

        public int PCoreCount => _pcoreCount;

        public int ECoreCount => _ecoreCount;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct SYSTEM_CPU_SET_INFORMATION
    {
        public uint Size;
        public CPU_SET_INFORMATION_TYPE Type;
        public CPU_SET Set;

        public override string ToString()
        {
            return $"{Set.LogicalProcessorIndex}: {Set.EfficiencyClass}";
        }

        public int Index
        {
            get { return Set.LogicalProcessorIndex; }
        }

        public bool IsPCore
        {
            get { return (int)Set.EfficiencyClass >= 1; }
        }

        public bool IsECore
        {
            get { return (int)Set.EfficiencyClass == 0; }
        }
    }
}

그래서 이를 이용해 다음과 같은 식으로 코딩할 수 있습니다.

using Console1;

internal class Program
{
    static void Main(string[] args)
    {
        CpuInfo cpuInfo = NativeMethods.GetSystemCpuSetInformation();
        if (cpuInfo.LogicalCoreCount == 0)
        {
            Console.WriteLine("failed to call Win32 API GetSystemCpuSetInformation");
            return;
        }

        Console.WriteLine($"IsHybridCPU: {cpuInfo.IsHybrid}");

        if (cpuInfo.IsHybrid)
        {
            Console.WriteLine($"# of PCore: {cpuInfo.PCoreCount}");
            Console.WriteLine($"# of ECore: {cpuInfo.ECoreCount}");

            Console.WriteLine();

            foreach (var item in cpuInfo)
            {
                Console.WriteLine($"[{item.Index}] IsPCore == {item.IsPCore}");
            }
        }
        else
        {
            Console.WriteLine($"# of Cores: {cpuInfo.LogicalCoreCount}");
        }
    }
}

제 컴퓨터에서 위의 코드를 실행하면 다음과 같은 식으로 출력합니다.

IsHybridCPU: True
# of PCore: 16
# of ECore: 8

[0] IsPCore == True
[1] IsPCore == True
[2] IsPCore == True
[3] IsPCore == True
[4] IsPCore == True
[5] IsPCore == True
[6] IsPCore == True
[7] IsPCore == True
[8] IsPCore == True
[9] IsPCore == True
[10] IsPCore == True
[11] IsPCore == True
[12] IsPCore == True
[13] IsPCore == True
[14] IsPCore == True
[15] IsPCore == True
[16] IsPCore == False
[17] IsPCore == False
[18] IsPCore == False
[19] IsPCore == False
[20] IsPCore == False
[21] IsPCore == False
[22] IsPCore == False
[23] IsPCore == False

보는 바와 같이 P-core가 16개, E-core가 8개입니다. 이를 위한 구분은 SYSTEM_CPU_SET_INFORMATION 구조체에 있는 EfficiencyClass 필드의 값을 이용하면 되는데요, Intel 문서에 보면,

This value represents the power-to-performance ratio of a logical processor. Cores with a higher Efficiency Class value in the EfficiencyClass field have higher performance but lower power efficiency.


EfficiencyClass의 값이 높을수록 고성능이면서 전력 소비는 (성능을 높임에 따라) 비효율적이라고 합니다. 현재는 PCore인 경우 1, ECore인 경우 0이 나오는데요, 이 값의 타입이 byte인 것을 감안하면 또 다른 값이 향후 추가될 여지가 있습니다.




이를 이용해서 ECore를 바쁘게 만들어볼까요? ^^ ProcessThread.ProcessorAffinity 속성과 함께라면 다음과 같이 ECore 수만큼의 스레드를 생성하고 일정 시간 무한 루프를 돌아 부하를 줄 수 있습니다.

public class CpuInfo : IEnumerable
{
    // ...[생략]...

    public void LoadAllEcore_And_SeeTaskManagerCpuInfo_ForSeconds(int loadSeconds)
    {
        if (IsHybrid == false)
        {
            return;
        }

        List<Thread> threads = new List<Thread>();
        EventWaitHandle startSignal = new EventWaitHandle(false, EventResetMode.ManualReset);

        foreach (var item in _list)
        {
            if (item.IsPCore == true)
            {
                continue;
            }

            Thread t = new Thread((obj) =>
            {
                if (obj == null)
                {
                    return;
                }

                int tid = (int)NativeMethods.GetCurrentThreadId();
                SetThreadAffinity(tid, (int)obj);

                startSignal.WaitOne();

                long started = Environment.TickCount64;
                while (true)
                {
                    long diff = Environment.TickCount64 - started;
                    if (diff / 1000 > loadSeconds)
                    {
                        break;
                    }
                }
            });

            threads.Add(t);
            t.Start(item.Index);
        }

        startSignal.Set();

        foreach (var item in threads)
        {
            item.Join();
        }
    }

    static void SetThreadAffinity(int threadId, int coreIndex)
    {
        foreach (ProcessThread thread in Process.GetCurrentProcess().Threads)
        {
            if (threadId == thread.Id)
            {
                if (OperatingSystem.IsWindows())
                {
                    thread.ProcessorAffinity = new IntPtr(1 << (coreIndex));
                    return;
                }
            }
        }
    }
}

위의 메서드를 호출하면 작업 관리자에서 다음과 같이 E-Core들의 사용량이 100%가 되는 것을 확인할 수 있습니다.

intel_pcore_1.png

그런데, 다소 이상한 점이 있습니다. 저렇게 E-core를 모두 바쁘게 만들었더니 윈도우 운영체제의 UI 반응 속도가 전체적으로 느려졌습니다. 분명히, P-core들은 놀고 있음에도 컴퓨터 사용이 힘들 정도로 성능이 낮아지는데, 어쩌면 윈도우 11의 UI 관련 동작들을 기본적으로 E-core에서 스케줄링이 되도록 만든 것이 아닌가... 할 정도입니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 2/15/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2023-06-21 08시52분
.NET에서 코어(Core) 관련 CPU 정보 알아내는 방법
; https://www.sysnet.pe.kr/2/0/960

How does Windows exploit hyperthreading?
; https://devblogs.microsoft.com/oldnewthing/20040913-00/?p=37883

Why is Windows using only even-numbered processors?
; https://devblogs.microsoft.com/oldnewthing/20230620-00/?p=108358
정성태

... 61  62  63  64  65  66  67  68  69  70  71  [72]  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
11847정성태3/17/201915206Linux: 7. 리눅스 C/C++ - 공유 라이브러리 동적 로딩 후 export 함수 사용 방법파일 다운로드1
11846정성태3/15/201913579Linux: 6. getenv, setenv가 언어/운영체제마다 호환이 안 되는 문제
11845정성태3/15/201914296Linux: 5. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) [3]
11844정성태3/14/201915124개발 환경 구성: 434. Visual Studio 2019 - 리눅스 프로젝트를 이용한 공유/실행(so/out) 프로그램 개발 환경 설정 [1]파일 다운로드1
11843정성태3/14/201910862기타: 75. MSDN 웹 사이트를 기본으로 영문 페이지로 열고 싶다면?
11842정성태3/13/201910269개발 환경 구성: 433. 마이크로소프트의 CoreCLR 프로파일러 예제를 Visual Studio CMake로 빌드하는 방법 [1]파일 다운로드1
11841정성태3/13/201910237VS.NET IDE: 132. Visual Studio 2019 - CMake의 컴파일러를 기본 g++에서 clang++로 변경
11840정성태3/13/201911340오류 유형: 526. 윈도우 10 Ubuntu App 환경에서는 USB 외장 하드 접근 불가
11839정성태3/12/201914109디버깅 기술: 124. .NET Core 웹 앱을 호스팅하는 Azure App Services의 프로세스 메모리 덤프 및 windbg 분석 개요 [3]
11838정성태3/7/201916862.NET Framework: 811. (번역글) .NET Internals Cookbook Part 1 - Exceptions, filters and corrupted processes [1]파일 다운로드1
11837정성태3/6/201926614기타: 74. 도서: 시작하세요! C# 7.3 프로그래밍 [10]
11836정성태3/5/201914428오류 유형: 525. Visual Studio 2019 Preview 4/RC - C# 8.0 Missing compiler required member 'System.Range..ctor' [1]
11835정성태3/5/201914190.NET Framework: 810. C# 8.0의 Index/Range 연산자를 .NET Framework에서 사용하는 방법 및 비동기 스트림의 컴파일 방법 [3]파일 다운로드1
11834정성태3/4/201913076개발 환경 구성: 432. Visual Studio 없이 최신 C# (8.0) 컴파일러를 사용하는 방법
11833정성태3/4/201913838개발 환경 구성: 431. Visual Studio 2019 - CMake를 이용한 공유/실행(so/out) 리눅스 프로젝트 설정파일 다운로드1
11832정성태3/4/201910871오류 유형: 524. Visual Studio CMake - rsync: connection unexpectedly closed
11831정성태3/4/201910458오류 유형: 523. Visual Studio 2019 - 새 창으로 뜬 윈도우를 닫을 때 비정상 종료
11830정성태2/26/201910244오류 유형: 522. 이벤트 로그 - Error opening event log file State. Log will not be processed. Return code from OpenEventLog is 87.
11829정성태2/26/201912155개발 환경 구성: 430. 마이크로소프트의 CoreCLR 프로파일러 예제 빌드 방법 - 리눅스 환경 [1]
11828정성태2/26/201918571개발 환경 구성: 429. Component Services 관리자의 RuntimeBroker 설정이 2개 있는 경우 [8]
11827정성태2/26/201912481오류 유형: 521. Visual Studio - Could not start the 'rsync' command on the remote host, please install it using your system package manager.
11826정성태2/26/201912362오류 유형: 520. 우분투에 .NET Core SDK 설치 시 패키지 의존성 오류
11825정성태2/25/201917210개발 환경 구성: 428. Visual Studio 2019 - CMake를 이용한 리눅스 빌드 환경 설정 [1]
11824정성태2/25/201912019오류 유형: 519. The SNMP Service encountered an error while accessing the registry key SYSTEM\CurrentControlSet\Services\SNMP\Parameters\TrapConfiguration. [1]
11823정성태2/21/201913508오류 유형: 518. IIS 관리 콘솔이 뜨지 않는 문제
11822정성태2/20/201911638오류 유형: 517. docker에 설치한 MongoDB 서버로 연결이 안 되는 경우
... 61  62  63  64  65  66  67  68  69  70  71  [72]  73  74  75  ...