Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 5개 있습니다.)
.NET Framework: 442. C# - 시스템의 CPU 사용량 및 프로세스(EXE)의 CPU 사용량 알아내는 방법
; https://www.sysnet.pe.kr/2/0/1684

Linux: 56. 리눅스 - /proc/pid/stat 정보를 이용해 프로세스의 CPU 사용량 구하는 방법
; https://www.sysnet.pe.kr/2/0/13215

Windows: 260. CPU 사용률을 나타내는 2가지 수치 - 사용량(Usage)과 활용률(Utilization)
; https://www.sysnet.pe.kr/2/0/13582

Windows: 261. CPU Utilization이 100% 넘는 경우를 성능 카운터로 확인하는 방법
; https://www.sysnet.pe.kr/2/0/13583

닷넷: 2233. C# - 프로세스 CPU 사용량을 나타내는 성능 카운터와 Win32 API
; https://www.sysnet.pe.kr/2/0/13589




CPU 사용률을 나타내는 2가지 수치 - 사용량(Usage)과 활용률(Utilization)

예전에 아래의 글을 쓰면서,

C# - 시스템의 CPU 사용량 및 프로세스(EXE)의 CPU 사용량 알아내는 방법
; https://www.sysnet.pe.kr/2/0/1684

CPU 사용률을 GetSystemTimes Win32 API를 이용해 구했고 그 수치가 "성능 모니터링 도구(Perfmon.msc)"를 시작했을 때 기본으로 보여주는 성능 카운터와 같았다고 했습니다.

[아래의 PerfCounter는 동일한 값을 반환]

Object: Processor Information
Counter: % Processor Time
Instance: _Total

Object: Processor
Counter: "%Processor Time
Instance: _Total)

그리고, 작업 관리자(좀 더 정확히는 [프로세스] 탭 및 [성능] 탭)의 CPU 수치는 아래의 성능 카운터와 같습니다.

[작업 관리자가 보여주는 CPU 사용률]

Object: Processor Information
Counter: % Processor Utility
Instance: _Total

위의 2가지 수치에 대해 나름 의미를 해석해 주는 글이 있어서 소개할까 하는데요, ^^

CPU 이용률의 두 가지 얼굴 – CPU 코어 사용량(Usage)과 활용률(Utilization)
; https://netmarble.engineering/cpu-core-usage-and-utilization/

그러니까, 위의 글에 따르면 성능 모니터링 도구에서 보여준 수치는 "CPU 사용량(Usage)"이고, 작업 관리자에서 봤던 (대체로 좀 더 높게 나오는) CPU 수치는 "CPU 활용률(Utilization)"이라는 것입니다.

  • CPU 사용량(Usage) - 시간 기반(Time-based)으로 측정되는 메트릭
  • CPU 활용률(Utilization) - 주파수 기반(Frequency-based)으로 측정되는 메트릭

그런데, 위의 글에서 소개하는 마이크로소프트의 공식 문서에 따르면,

CPU usage exceeds 100% in Task Manager and Performance Monitor if Intel Turbo Boost is active
; https://learn.microsoft.com/en-us/troubleshoot/windows-client/performance/cpu-usage-exceeds-100

후자의 경우 주파수를 이용한 설명은 해도 "주파수 기반"이라는 용어는 사실 등장하지 않습니다. 그보다는 다음과 같이 2개의 기준으로 분류하고 있습니다.

  1. time-based performance counters - measure the percentage of time that the processor is busy
  2. utility performance counters - measure how much work the processor actually performs

마이크로소프트의 저 문서를 보니까, 이제야 좀 2개의 수치에 대한 차이가 눈에 들어옵니다. ^^ 간단하게 설명해 볼까요?

우선, time-based는 말 그대로 CPU가 (속도와 상관없이) 일하고 있는 시간을 기준으로 측정한 값입니다. 계산을 간단하게 하기 위해 단일 코어 CPU가 1초 동안 500ms만큼 busy 상태로 있었다면 50%의 사용률을 나타내는 것입니다.

반면 utility는 같은 시간을 소모했더라도 CPU의 실행 주파수가 달라지는 것을 반영한 값입니다. 예를 들어, (CPU 구매 시 표기된) 명목 주파수가 1GHz인 CPU가 Turbo Boost 상태의 2GHz로 1초 동안 내내 실행되었다면 200%까지 일을 한 것입니다. (하지만 작업 관리자는 100%로 절삭합니다.)

결국 주파수가 변하지 않는 CPU였다면 time-based와 utility는 같은 값을 나타낼 수 있었겠지만, 동작 주파수가 달라지는 근래의 환경에서는 저 2개의 수치가 상이해질 수밖에 없는 것입니다.

대충 이해가 되셨죠? ^^




"CPU 이용률의 두 가지 얼굴 – CPU 코어 사용량(Usage)과 활용률(Utilization)" 글 덕분에 2개의 차이점을 쉽게 알 수 있었는데요, 대신 해당 글에서 약간 동의할 수 없는 내용이 있어 이에 대한 부가적인 기록을 남깁니다.

우선, time-based 수치를 설명하면서 들었던 공식을 보면,

%ProcessorTime=1−(B/T)

    B : 측정 범위 시간 내에 논리 프로세서에서 "유휴(Idle) 스레드"가 소비하는 100ns 간격의 수(샘플링 횟수)
    T : 측정 범위 시간 내에 100ns 간격의 수 (샘플링 횟수)

위의 내용에 대한 참고 문서를 밝히지 않아 현재는 전적으로 신뢰할 수 없는 내용으로 보입니다. 사실, 커널의 스레드 스케줄러는 하드웨어 자원인 CPU에 Idle 스레드를 얹고 해제하는 것을 담당합니다. 즉, 정확히 그 시간을 알고 있기 때문에 굳이 100ns 간격으로 샘플링을 해야 할 필요가 없습니다.

성능 모니터링 도구의 설명에서도,

It is calculated by measuring the percentage of time that the processor spends executing the idle thread and then subtracting that value from 100%.


딱히 100ns나 sampling 횟수라는 단어는 쓰고 있지 않습니다. 혹시 이에 대해 공식 문서를 알고 계시다면 덧글 부탁드립니다. ^^

그다음으로, utility 수치를 구하면서 제시한 설명을 보면,

%ProcessorUtility=(E/B)
    E : 주어진 시간 범위 내에 논리 프로세서의 유효 주파수(Processor Effective Frequency)
    B : 논리 프로세서의 기본 주파수(Processor Base Frequency)

유효 주파수는 실제 동작한 CPU 클럭 사이클 수를 의미하며 RDTSC 함수를 이용해서 계산할 수 있고, 기본 주파수 정보는 PROCESSOR_POWER_INFORMATION 구조체를 인자로 한 NtPowerInformation API 호출을 통해서 구할 수 있습니다.


"E" 수치를 rdtsc로 계산할 수 있다고 했는데요, 사실 이것은 전에도 제가 설명했듯이,

윈도우 운영체제의 시간 함수 (5) - TSC(Time Stamp Counter)와 QueryPerformanceCounter
; https://www.sysnet.pe.kr/2/0/11068

근래의 CPU는 모두 Invariant TSC를 지원하기 때문에 CPU의 실제 동작 주파수에 상관없이 rdtsc는 보정된 값을 반환합니다. 게다가, rdtsc는 유효 명령어 수행과 관계없이 설정된 주파수로 증가하기 때문에 이 값을 분자로 둘 수 없습니다. 이에 대한 테스트를 다음의 코드로 확인하는 것도 가능합니다.

#include <stdio.h>
#include <Windows.h>
#include <ctime>

#pragma comment(lib, "winmm.lib")

int main()
{
    int count = 100;

    const DWORD msDuration = 1000;
    const int iterations = 36;

    for (int i = 0; i < iterations; ++i)
    {
        __int64 rdtscStart = __rdtsc(); // 현재의 cpu cycle 수를 구하고,

        Sleep(1000);

        __int64 rdtscElapsed = __rdtsc() - rdtscStart; // 1초가 지난 후 cpu cycle 수를 구함

        printf("  elapsed: %I64d, %0.4fGHz\n", rdtscElapsed, rdtscElapsed / 1000.0 / 1000 / 1000);
    }

    return 0;
}

위와 같이 Thread Sleep을 1초 정도 준 다음 그 사이의 rdtsc를 값을 구하면, 제 컴퓨터의 i9-12900K CPU 환경에서는 다음과 같은 결과가 나옵니다.

  elapsed: 3187455039, 3.1875GHz
  elapsed: 3190156222, 3.1902GHz
  elapsed: 3188967666, 3.1890GHz
  elapsed: 3187236801, 3.1872GHz
  elapsed: 3191939036, 3.1919GHz
  elapsed: 3188609006, 3.1886GHz
...[생략]...

보시다시피 "Performance-core 기본 주파수"에 해당하는 3.2GHz로 동작하고 있습니다.

따라서, 결국 저 공식은 3.2/3.2로 계산이 돼 100% CPU 사용률이라는 잘못된 결과가 나옵니다. 이를 통해 "E"는 rdtsc로 구한 값이라기보다는 오히려 "Effective"라는 말에 유의해 해석해야 합니다. 즉, CPU가 운영체제에 의해 할당된 작업을 수행하는 용도로 순수하게 소비한 주파수만을 유효하게 계산식에 포함할 때 의미가 있는 것입니다.

간단하게 예를 들면, 2GHz의 명목 주파수를 갖는 CPU가 지난 1초 동안 5GHz로 동작했는데 그중 500ms 동안 운영체제에 의해 작업이 할당돼 실행이 되었다면 실질적으로 2.5GHz에 해당하는 주파수가 명령어를 수행하는 데 사용됐으므로 2.5/2로 계산해 125%의 사용률이 나오는 것입니다.

마지막으로 한 가지만 더 문제를 제기한다면, 해당 글에서는 저렇게 time-based/utility 메트릭을 이해해야 하는 이유를 마지막 즈음에 "모니터링" 절을 통해 이렇게 설명하고 있습니다.

앞서 설명해 드린 것처럼, CPU 코어 사용량(Usage)은 논리 프로세서가 스레드 — System Idle Process가 아닌 Application Process가 생성한 스레드 ― 실행에 소비하는 시간을 의미합니다. 이는 논리 프로세서에서 실행 가능한 스레드(Ready or Running 상태의 스레드)가 소비하는 시간을 나타냅니다. 따라서 CPU 코어 사용량(Usage)만으로는 논리 프로세서에서 “대기(Waiting)” 상태의 스레드와 관련된 CPU 자원 소모 정보를 확인할 수 없습니다. 그러므로 CPU 코어 활용률(Utilization) 메트릭을 이용하여 “대기(Waiting)” 상태의 스레드와 관련된 불필요한 CPU 자원 소모 여부를 확인하는 것이 중요합니다.


왜? utility 수치가 Waiting 상태의 스레드와 관련된 불필요한 CPU 자원 소모와 연결이 되는 것일까요? 운영체제는 사용자 모드의 코드를 수행하는 것뿐만 아니라 커널 코드까지 포함해 time-based/utility에 대한 CPU 사용량을 제공합니다. 즉, Waiting 상태의 스레드가 많다고 해서 특별히 utility 메트릭의 값이 time-based에 비해 바뀐 값을 나타내는 것은 아닙니다.




개인적으로 위의 내용을 정리하면서, 한 가지 궁금한 점이 생겼습니다. 우선, time-based 메트릭의 경우는 운영체제가 구간 사이의 흐른 시간 및 스레드를 CPU에 할당한 시간을 알고 있다는 점에서, 계산 과정이 쉽게 납득이 갑니다.

그런데, 주파수는 이야기가 좀 다릅니다. 운영체제 입장에서 현재 CPU(코어)의 동작 주파수를 정확히 알고 있어야만 하는데, 이게 가능하려면 1) CPU가 주파수를 조정할 때마다 운영체제에게 Interrupt로 알리든가, 2) 운영체제가 애당초 CPU의 주파수도 조정하든가... 해야 합니다.

하드웨어를 잘 아시는 분은 ^^ 알고 계시겠지만, 소프트웨어 개발자인 저는 저 부분을 몰라서 검색해야만 했습니다. 그 결과, 아래의 글을 찾았는데요,

System crashes and you cannot change CPU frequency on Intel SKL platform in Windows 8.1 or Windows Server 2012 R2
; https://support.microsoft.com/en-us/topic/system-crashes-and-you-cannot-change-cpu-frequency-on-intel-skl-platform-in-windows-8-1-or-windows-server-2012-r2-529d5e8b-0c1b-4568-dc54-21eccc2f23a7

The operating system cannot change CPU frequency to reduce power consumption. Therefore, CPU always runs on the maximum possible frequency that may affect the device battery life and heat dissipation.
To resolve this issue, we have released a hotfix that contains the Intelppm.sys file.
Note The Intelppm.sys file is the CPU driver of Skylake. There are other drivers in the same installation component with the Intelppm.sys file in this hotfix.


즉, 윈도우 운영체제가 (인텔의 경우) Intelppm.sys 드라이버를 이용해 CPU의 동작 주파수를 조정한다는 것입니다. 만약 CPU 스스로 조절하는 것이었다면 "CPU always runs on the maximum possible frequency"라는 문구는 없었겠죠? ^^

보다 공식적인 문서가 있나 좀 더 검색을 해봤는데요,

Enhanced Intel® SpeedStep® Technology for the Intel® Pentium® M Processor 
; https://download.intel.com/design/network/papers/30117401.pdf

All Intel Pentium M processors have support for Enhanced Intel SpeedStep Technology. This can be
verified by checking the ECX feature bit 07 in the CPUID register. Enhanced Intel SpeedStep Technology
is enabled by setting the IA32_MISC_ENABLE MSR, bit 16. This bit should be written by the system
BIOS upon boot.

Processor frequency/voltage transitions are initiated by writing a 16-bit value to the IA32_PERF_CTL
register. If a transition is already in progress, transition to a new value will take effect subsequently. Reads
of the IA32_PERF_CTL register determine the last targeted operating point. The current operating point
can be read from IA32_PERF_STATUS register, which is updated dynamically.

By centralizing the implementation of Enhanced Intel SpeedStep Technology to the processor, software can perform a frequency/voltage operating state transition by simply writing to one register


저렇게 Intel 측에서 제공하는 레지스터에 값을 기록함으로써 주파수(및 동작 전압)을 조절할 수 있다고 합니다.




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







[최초 등록일: ]
[최종 수정일: 3/29/2024]

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

비밀번호

댓글 작성자
 




[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13838정성태12/4/2024653오류 유형: 935. Windbg - Breakpoint 0's offset expression evaluation failed.
13837정성태12/3/2024757디버깅 기술: 204. Windbg - 윈도우 핸들 테이블 (3) - Windows 10 이상인 경우
13836정성태12/3/2024985디버깅 기술: 203. Windbg - x64 가상 주소를 물리 주소로 변환 (페이지 크기가 2MB인 경우)
13835정성태12/2/20241002오류 유형: 934. Azure - rm: cannot remove '...': Directory not empty
13834정성태11/29/20241090Windows: 275. C# - CUI 애플리케이션과 Console 윈도우 (Windows 10 미만의 Classic Console 모드인 경우)파일 다운로드1
13833정성태11/29/20241099개발 환경 구성: 737. Azure Web App에서 Scale-out으로 늘어난 리눅스 인스턴스에 SSH 접속하는 방법
13832정성태11/27/20241111Windows: 274. Windows 7부터 도입한 conhost.exe
13831정성태11/27/2024979Linux: 111. eBPF - BPF_MAP_TYPE_PERF_EVENT_ARRAY, BPF_MAP_TYPE_RINGBUF에 대한 다양한 용어들
13830정성태11/25/20241087개발 환경 구성: 736. 파이썬 웹 앱을 Azure App Service에 배포하기
13829정성태11/25/20241027스크립트: 67. 파이썬 - Windows 버전에서 함께 설치되는 py.exe
13828정성태11/25/20241048개발 환경 구성: 735. Azure - 압축 파일을 이용한 web app 배포 시 디렉터리 구분이 안 되는 문제파일 다운로드1
13827정성태11/25/20241133Windows: 273. Windows 환경의 파일 압축 방법 (tar, Compress-Archive)
13826정성태11/21/20241206닷넷: 2313. C# - (비밀번호 등의) Console로부터 입력받을 때 문자열 출력 숨기기(echo 끄기)파일 다운로드1
13825정성태11/21/20241161Linux: 110. eBPF / bpf2go - BPF_RINGBUF_OUTPUT / BPF_MAP_TYPE_RINGBUF 사용법
13824정성태11/20/20241108Linux: 109. eBPF / bpf2go - BPF_PERF_OUTPUT / BPF_MAP_TYPE_PERF_EVENT_ARRAY 사용법
13823정성태11/20/20241085개발 환경 구성: 734. Ubuntu에 docker, kubernetes (k3s) 설치
13822정성태11/20/20241045개발 환경 구성: 733. Windbg - VirtualBox VM의 커널 디버거 연결 시 COM 포트가 없는 경우
13821정성태11/18/20241172Linux: 108. Linux와 Windows의 프로세스/스레드 ID 관리 방식
13820정성태11/18/20241144VS.NET IDE: 195. Visual C++ - C# 프로젝트처럼 CopyToOutputDirectory 항목을 추가하는 방법
13819정성태11/15/20241154Linux: 107. eBPF - libbpf CO-RE의 CONFIG_DEBUG_INFO_BTF 빌드 여부에 대한 의존성
13818정성태11/15/20241232Windows: 272. Windows 11 24H2 - sudo 추가
13817정성태11/14/20241103Linux: 106. eBPF / bpf2go - (BPF_MAP_TYPE_HASH) Map을 이용한 전역 변수 구현
13816정성태11/14/20241174닷넷: 2312. C#, C++ - Windows / Linux 환경의 Thread Name 설정파일 다운로드1
13815정성태11/13/20241102Linux: 105. eBPF - bpf2go에서 전역 변수 설정 방법
13814정성태11/13/20241215닷넷: 2311. C# - Windows / Linux 환경에서 Native Thread ID 가져오기파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...