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

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
정성태

... 16  17  18  19  20  21  [22]  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13086정성태6/22/20228056.NET Framework: 2026. C# 11 - 문자열 보간 개선 2가지파일 다운로드1
13085정성태6/22/20228140.NET Framework: 2025. C# 11 - 원시 문자열 리터럴(raw string literals)파일 다운로드1
13084정성태6/21/20226706개발 환경 구성: 644. Windows - 파이썬 2.7을 msi 설치 없이 구성하는 방법
13083정성태6/20/20227345.NET Framework: 2024. .NET 7에 도입된 GC의 메모리 해제에 대한 segment와 region의 차이점 [2]
13082정성태6/19/20226368.NET Framework: 2023. C# - Process의 I/O 사용량을 보여주는 GetProcessIoCounters Win32 API파일 다운로드1
13081정성태6/17/20226429.NET Framework: 2022. C# - .NET 7 Preview 5 신규 기능 - System.IO.Stream ReadExactly / ReadAtLeast파일 다운로드1
13080정성태6/17/20227049개발 환경 구성: 643. Visual Studio 2022 17.2 버전에서 C# 11 또는 .NET 7.0 preview 적용
13079정성태6/17/20224669오류 유형: 814. 파이썬 - Error: The file/path provided (...) does not appear to exist
13078정성태6/16/20226801.NET Framework: 2021. WPF - UI Thread와 Render Thread파일 다운로드1
13077정성태6/15/20227106스크립트: 40. 파이썬 - PostgreSQL 환경 구성
13075정성태6/15/20226049Linux: 50. Linux - apt와 apt-get의 차이 [2]
13074정성태6/13/20226384.NET Framework: 2020. C# - NTFS 파일에 사용자 정의 속성값 추가하는 방법파일 다운로드1
13073정성태6/12/20226616Windows: 207. Windows Server 2022에 도입된 WSL 2
13072정성태6/10/20226894Linux: 49. Linux - ls 명령어로 출력되는 디렉터리 색상 변경 방법
13071정성태6/9/20227516스크립트: 39. Python에서 cx_Oracle 환경 구성
13070정성태6/8/20227323오류 유형: 813. Windows 11에서 입력 포커스가 바뀌는 문제 [1]
13069정성태5/26/20229571.NET Framework: 2019. C# - .NET에서 제공하는 3가지 Timer 비교 [2]
13068정성태5/24/20228035.NET Framework: 2018. C# - 일정 크기를 할당하는 동안 GC를 (가능한) 멈추는 방법 [1]파일 다운로드1
13067정성태5/23/20227320Windows: 206. Outlook - 1년 이상 지난 메일이 기본적으로 안 보이는 문제
13066정성태5/23/20226671Windows: 205. Windows 11 - Windows + S(또는 Q)로 뜨는 작업 표시줄의 검색 바가 동작하지 않는 경우
13065정성태5/20/20227326.NET Framework: 2017. C# - Windows I/O Ring 소개 [2]파일 다운로드1
13064정성태5/18/20226922.NET Framework: 2016. C# - JIT 컴파일러의 인라인 메서드 처리 유무
13063정성태5/18/20227352.NET Framework: 2015. C# - 인라인 메서드(inline methods)
13062정성태5/17/20228082.NET Framework: 2014. C# - async/await 그리고 스레드 (4) 비동기 I/O 재현파일 다운로드1
13061정성태5/16/20226923.NET Framework: 2013. C# - FILE_FLAG_OVERLAPPED가 적용된 파일의 읽기/쓰기 시 Position 관리파일 다운로드1
13060정성태5/15/20229440.NET Framework: 2012. C# - async/await 그리고 스레드 (3) Task.Delay 재현파일 다운로드1
... 16  17  18  19  20  21  [22]  23  24  25  26  27  28  29  30  ...