Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1162. C# - 인텔 CPU의 P-Core와 E-Core를 구분하는 방법 [링크 복사], [링크+제목 복사],
조회: 10434
글쓴 사람
정성태 (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)
11897정성태5/16/201920640오류 유형: 536. Visual Studio - "Developer Pack"을 설치했는데도 "대상 프레임워크" 목록에 나오지 않는 경우 [2]
11896정성태5/15/201915353개발 환경 구성: 440. C#, C++ - double의 Infinity, NaN 표현 방식파일 다운로드1
11895정성태5/12/201913759.NET Framework: 832. ML.NET Model Builder - 회귀(Regression), 다중 분류(Multi-class classification) 예제파일 다운로드1
11894정성태5/10/201914914VS.NET IDE: 135. Visual Studio - ML.NET Model Builder 소개 [5]
11893정성태5/10/201912518오류 유형: 535. C# 6.0 이상의 문법을 컴파일 시 오류가 발생한다면?
11892정성태5/10/201912558웹: 38. HTTP Cookie의 expires 시간 형식(RFC7231)
11891정성태5/9/201915040.NET Framework: 831. (번역글) .NET Internals Cookbook Part 12 - Memory structure, attributes, handles
11890정성태5/8/201910788개발 환경 구성: 439. "Visual Studio Enterprise is required to execute the test." 메시지와 관련된 코드 기록
11889정성태5/8/201911644개발 환경 구성: 438. mstest, QTAgent의 로그 파일 설정 방법
11888정성태5/8/201926878.NET Framework: 830. C# - 비동기 호출을 취소하는 CancellationToken의 간단한 예제 코드 [1]파일 다운로드1
11887정성태5/8/201913369.NET Framework: 829. C# - yield 문을 사용할 수 있는 메서드의 조건
11886정성태5/7/201912434오류 유형: 534. mstest.exe 실행 시 "Visual Studio Enterprise is required to execute the test." 오류 [2]
11885정성태5/7/20199397오류 유형: 533. mstest.exe 실행 시 "File extension specified '.loadtest' is not a valid test extension." 오류 발생
11884정성태5/5/201913079.NET Framework: 828. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 두 번째 이야기
11883정성태5/3/201917393.NET Framework: 827. C# - 인터넷 시간 서버로부터 받은 시간을 윈도우에 적용하는 방법파일 다운로드1
11882정성태5/2/201914309.NET Framework: 826. (번역글) .NET Internals Cookbook Part 11 - Various C# riddles파일 다운로드1
11881정성태4/28/201914550오류 유형: 532. .NET Core 프로젝트로 마이그레이션 시 "CS0579 Duplicate 'System.Reflection.AssemblyCompanyAttribute' attribute" 오류 발생
11880정성태4/25/201911244오류 유형: 531. 이벤트 로그 오류 - Task Scheduling Error: m->NextScheduledSPRetry 1547, m->NextScheduledEvent 1547
11879정성태4/24/201916483.NET Framework: 825. (번역글) .NET Internals Cookbook Part 10 - Threads, Tasks, asynchronous code and others파일 다운로드2
11878정성태4/22/201914472.NET Framework: 824. (번역글) .NET Internals Cookbook Part 9 - Finalizers, queues, card tables and other GC stuff파일 다운로드1
11877정성태4/22/201914452.NET Framework: 823. (번역글) .NET Internals Cookbook Part 8 - C# gotchas파일 다운로드1
11876정성태4/21/201914265.NET Framework: 822. (번역글) .NET Internals Cookbook Part 7 - Word tearing, locking and others파일 다운로드1
11875정성태4/21/201914631오류 유형: 530. Visual Studo에서 .NET Core 프로젝트를 열 때 "One or more errors occurred." 오류 발생
11874정성태4/20/201914651.NET Framework: 821. (번역글) .NET Internals Cookbook Part 6 - Object internals파일 다운로드1
11873정성태4/19/201913594.NET Framework: 820. (번역글) .NET Internals Cookbook Part 5 - Methods, parameters, modifiers파일 다운로드1
11872정성태4/17/201914071.NET Framework: 819. (번역글) .NET Internals Cookbook Part 4 - Type members파일 다운로드1
... 61  62  63  64  65  66  67  68  69  [70]  71  72  73  74  75  ...