PerfView를 활용한 .NET GC 프로파일링

🧐 | 2023-05-09

안녕하세요, 넷마블 TPM실 아키텍처최적화팀 박정욱입니다.

서비스의 CPU와 메모리 관리는 두말할 필요가 없을 정도로 중요합니다. 특히 수많은 객체가 생성 및 소멸되는 게임 서버라면 CPU와 메모리 관리는 게임 서버의 성능을 좌우하는 중요 요소가 되죠. 최근 .NET 기술이 발전하고 안정화되면서 C# 기반의 게임 서버도 많이 사용합니다. 이때 핵심은 C#의 GC(Garbage Collection)을 프로파일링해 어떤 객체가 성능에 영향을 주는지 면밀하게 파악하는 것입니다.

이러한 GC 프로파일링에 활용할 수 있는 좋은 도구로 PerfView가 있습니다. 이번에는 PerfView를 이용해서 .NET GC 프로파일링을 할 때 필요한 기본 설정과 다양한 옵션을 소개해 보려고 합니다.

개요

PerfView는 Microsoft에서 개발한 오픈소스 성능 분석 툴로서 CPU와 메모리 관련 성능 이슈를 분석할 수 있습니다. 특히 .NET GC 성능 이슈 분석 시 매우 유용한 정보를 얻을 수 있습니다.

.NET의 성능 진단 툴에는 dotnet-gcdump와 dotnet-counters가 있습니다. dotnet-gcdump의 경우 객체 할당 정보와 해당 객체의 참조 정보를 확인할 수 있고, dotnet-counters의 경우 실시간으로 GC 힙 정보를 확인할 수 있습니다. 반면 PerfView는 GC 발생 시기, GC 발생 사유, GC가 수집한 메모리 크기, 객체 할당 콜스택 등의 정보를 확인할 수 있습니다.

설치

PerfView는 단일 실행 파일로 구성되어 있어서 별도의 설치 과정 없이 다운로드 후 바로 실행이 가능합니다. 실행 파일은 Microsoft의 PerfView GitHub에서 다운로드할 수 있습니다.

성능 데이터 수집

PerfView는 GUI 모드와 CLI 모드로 성능 데이터를 수집할 수 있습니다. 본 문서의 설명은 CLI 모드 사용을 전제로 작성되었습니다.

0. 성능 데이터를 수집할 대상 프로세스를 실행합니다.

1. PerfView가 설치된(다운로드된) 폴더로 이동하여 CMD 또는 PowerShell을 실행합니다.

2. 성능 데이터 수집을 시작하기 위해 다음 명령을 실행합니다.

PS > .\PerfView.exe collect /NoGui /AcceptEULA /KernelEvents=Profile+Process+Thread+ImageLoad+ContextSwitch /ClrEvents:GC+Stack /BufferSizeMB:3000 /CircularMB:3000 /MaxCollectSec:100 /Process:gc-stress-test

위 실행 옵션은 GC 성능 데이터 수집을 위해 권장하는 옵션입니다. 실행 옵션에 대한 자세한 설명은 ‘성능 데이터 수집 실행 옵션’을 참고하기 바랍니다.

3. PerfView 프로세스가 실행되고 성능 데이터 수집이 시작됩니다. 지정된 시간(2번의 실행 옵션에서는 /MaxCollectSec:100)이 지나면 자동으로 수집이 종료됩니다.

4. 수집이 완료되면 [Enter] 키를 눌러서 PerfView 프로세스를 종료합니다.

5. PerfView가 설치된(다운로드된) 폴더에서 PerfViewData.etl.zip 파일이 생성되었는지 확인합니다.

PerfView의 성능 데이터 분석은 PerfViewData.etl.zip를 기반으로 이뤄집니다. 성능 데이터 분석에서 자세하게 설명합니다.

성능 데이터 수집 실행 명령 및 옵션

앞에서 소개했던 성능 데이터 수집 실행 명령에는 다양한 옵션이 존재합니다. 여기에서는 자주 사용하거나 꼭 필요한 옵션 중심으로 무엇이 있는지 설명하겠습니다.

collect

collect는 성능 데이터를 수집하는 명령입니다.

/NoGui

/NoGui는 GUI 대신 콘솔 창을 실행시켜 PerfView가 CLI 모드로 작동하도록 만드는 명령입니다.

/AcceptEULA

/AcceptEULA는 PerfView 사용을 위한 최종 사용자 사용권 계약(EULA) 수락한다는 의미입니다.

/KernelEvents

/KernelEvents는 수집할 커널 이벤트 종류를 지정합니다. 해당 명령의 옵션은 ‘=’을 입력한 후 설정하며 ‘+’를 입력해 옵션 각각을 구분합니다.

  • Profile: 1msec마다 CPU 실행 정보를 수집하고 CPU 스택 추적을 수행합니다. 이 옵션이 포함되어야 GCStats 항목에서 GC와 관련된 시간 정보가 수집됩니다.
  • Process: 프로세스의 생성과 소멸 정보를 수집합니다.
  • Thread: 스레드의 생성과 소멸 정보를 수집합니다.
  • ImageLoad: DLL 또는 EXE가 메모리에 로드되는 정보를 수집합니다(예: LoadLibaryEx 호출)
  • ContextSwitch: Context Switch 정보를 수집합니다.
  • Dispatcher: 스레드의 Waiting 상태 유발 정보를 수집합니다.

/ClrEvents

/ClrEvents는 .NET 프로세스에서 생성하는 CLR ETW Events 정보를 수집하는 명령입니다. 해당 명령의 옵션은 ‘:’을 입력한 후 설정하며, ‘+’를 입력해 옵션 각각을 구분합니다. 자주 사용하는 옵션은 다음과 같습니다.

  • GC: GC 시작과 종류 정보를 수집합니다.
  • Stack: GC 객체 할당에 대한 스택 추적을 활성화하고 다양한 런타임(CLR) 이벤트 정보를 수집합니다.

/Providers

/Providers는 추가적인 EventSource 제공자를 지정하는 명령입니다. 해당 명령의 옵션은 ‘=’을 입력한 후 설정하며, ‘,’를 입력해 옵션 각각을 구분합니다. 자주 사용하는 옵션에는 런타임(CLR) Private 이벤트와 연관된 스택 정보를 수집하는 ClrPrivate:@StacksEnabled=true가 있습니다. 다음 사항을 고려해 사용하기를 권장합니다.

  • Pinned 객체에 대해서 Pinning이 발생한 콜스택 정보를 수집할 수 있습니다.
  • 이 옵션을 활성화할 경우 수집되는 데이터 용량이 매우 커집니다. BufferSizeMB 제한으로 소실되는 이벤트가 발생할 수 있으며 CircularMB 제한으로 콜스택 정보가 깨질 수 있습니다. 이럴 경우 BufferSizeMBCircularMB의 값을 충분히 크게 지정해야 합니다.
  • 고정된 각 개체에 대해 고정이 발생한 정확한 스택에 대한 추가 정보를 제공합니다.

/BufferSizeMB

/BufferSizeMB는 수집된 데이터가 파일로 기록되기 전에 임시 저장되는 버퍼 사이즈를 지정합니다. 다음 사항을 고려해 사용하기를 권장합니다.

  • 단위는 MB이며 기본값은 256MB입니다.
  • GC 성능 데이터 수집 시에는 3000MB 이상을 권장합니다.

/CircularMB

/CircularMB는 데이터 파일의 크기가 너무 커지지 않도록 파일 사이즈를 지정합니다. 다음 사항을 고려해 사용하기를 권장합니다.

  • GC 성능 데이터 수집 파일의 크기가 지정된 파일 크기보다 크다면 가장 오래된 데이터가 덮어씌워집니다.
  • 단위는 MB이며 기본값은 500MB입니다.
  • GC 성능 데이터 수집 시에는 3000MB 이상을 권장합니다.

/MaxCollectSec

/MaxCollectSec는 최대 수집 시간을 지정합니다. 지정된 시간이 지나면 자동으로 수집이 종료됩니다.

이 값을 지정하지 않으면 수동으로 수집을 종료해야 함에 주의합니다.

/Process

/Process는 수집 대상 프로세스의 PID 또는 프로세스 이름을 지정합니다. 다음 사항을 고려해 사용하기를 권장합니다.

  • PerfView는 기본적으로 OS상의 모든 프로세스를 대상으로 성능 데이터를 수집합니다.
  • 수집 대상 프로세스를 명시적으로 지정할 경우 지정된 대상 프로세스에서만 Non-Kernel 이벤트 정보를 수집하여 성능 수집 오버헤드와 파일 크기를 줄일 수 있습니다.

권장 옵션에 대한 자세한 설명 및 추가 옵션에 대한 설명은 PerfView.exe를 GUI 모드에서 실행한 후 [Help] → [Users Guide] 항목을 참고하기 바랍니다.

성능 데이터 분석

수집된 성능 데이터 파일은 PerfView가 실행된(설치된) 폴더에 ‘PerfViewData.etl.zip’라는 이름으로 저장됩니다. 이제 이 파일로 성능 데이터를 분석할 차례입니다.

성능 데이터 분석은 크게 GCStats 분석, GC Heap Alloc Ignore Free (Coarse Sampling) Stacks 분석, Pinning At GC Time Stacks 분석으로 나눌 수 있습니다. 해당 과정을 하나씩 살펴보겠습니다.

PerfView GUI 모드 사용하기

성능 데이터를 분석하기 위해서는 먼저 필요한 분석 데이터를 선택해야 합니다. 이때는 PerfView의 GUI 모드를 사용하는 것이 좋습니다. GUI 모드에서 원하는 분석 데이터를 선택하는 방법은 다음과 같습니다.

1. PerfView.exe를 실행합니다.

2. PerfView에서 왼쪽 폴더 영역을 사용하여 성능 데이터 분석에 사용할 ‘.etl.zip’라는 파일을 찾습니다. 왼쪽 폴더 영역은 현재 디렉터리와 PerfView가 분석한 정보를 담은 파일을 표시합니다. 디렉터리를 변경하려면 목록에서 하위 디렉터리를 선택하거나 폴더 영역의 상단에있는 텍스트 상자에 원하는 디렉터리(예: c:\Perflogs) 경로를 입력합니다.

3. 분석 정보를 보려는 ‘.etl.zip’ 파일을 더블 클릭합니다. 왼쪽 폴더 영역에서 선택한 ‘.etl.zip’ 파일 아래에 여러 분석 항목이 나타납니다.

4. 여러 분석 항목 중 원하는 정보를 보려면 해당 항목을 더블 클릭합니다. 그중 주요 참고할 항목은 다음과 같습니다.

  • GC 발생 시기, GC 발생 사유, GC가 수집한 메모리 크기 등의 GC 힙 분석 정보는 [Memory] 그룹 항목 하단의 [GCStats] 항목에서 볼 수 있습니다.
  • 객체 할당 콜스택 정보는 [Memory] 그룹 항목 하단의 [GC Heap Alloc Ignore Free (Coarse Sampling) Stacks] 항목에서 볼 수 있습니다.

GCStats 분석

GCStats 분석은 GC 발생 시기, GC 발생 사유, GC가 수집한 메모리 크기 등의 GC 힙 분석이 가능한 항목입니다.

[Memory] 그룹 항목 하단에서 [GCStats] 항목을 더블 클릭합니다. 별도의 창이 실행되면서 최상단에 아래와 같은 항목이 표시됩니다.

PerfView는 OS상에 실행 중인 모든 프로세스로부터 데이터를 수집하기 때문에 같은 분석 항목이 프로세스별로 표시됩니다.

분석 대상 프로세스를 확인하고 해당 항목을 클릭합니다. 대상 프로세스에 대한 다음과 같은 자세한 정보가 표시됩니다.

GCStats 분석의 주요 항목

GCStats 분석에는 여러 가지 분석 항목들이 있습니다. 여기에서는 꼭 참고해야 할 핵심 항목 중심으로 분석 항목이 무엇인지를 소개합니다.

Commandline

해당 프로세스를 실행할 때 사용된 명령어입니다.

Runtime Version

.NET 런타임 버전을 나타냅니다.

CLR Startup Flags

CLR 실행 플래그를 의미합니다. CLR은 Common Language Runtime의 줄임말입니다.

Total CPU Time

총 CPU 수행 시간입니다. 머신 CPU Core들의 전체 수행 시간 중 해당 프로세스가 사용한 CPU 시간입니다. 따라서 성능 데이터 수집 시간(/MaxCollectSec:100)과 일치하지 않습니다.

성능 데이터 수집시 /KernelEvents=Profile를 지정하지 않으면 이 값은 0으로 표시됩니다.

Total GC CPU Time

총 GC CPU 수행 시간입니다. 머신 CPU Core들의 전체 수행 시간 중 프로세스가 GC 처리에 사용한 CPU 시간입니다. 따라서 성능 데이터 수집 시간(/MaxCollectSec:100)과 일치하지 않습니다.

성능 데이터 수집시 /KernelEvents=Profile를 지정하지 않으면 이 값은 0으로 표시됩니다.

Total Allocs

성능 데이터 수집 동안 해당 프로세스에서 발생한 총 메모리 할당량입니다.

GC CPU MSec/MB Alloc

MB 당 GC CPU 수행 시간입니다. 해당 시간은 Total GC CPU time / Total Allocs이라는 계산식을 이용해 구합니다.

성능 데이터 수집시 /KernelEvents=Profile를 지정하지 않으면 이 값은 0으로 표시됩니다.

Total GC Pause

GC에 의해 중단된 총시간입니다. 이 값에는 GC가 시작되기 전 관리 스레드를 중단(Suspend)하는 데 걸리는 시간이 포함되어 있습니다.

% Time paused for Garbage Collection(% Pause time in GC)

GC에 의해 중단된 총시간 비율 수치입니다. 이 값은 Total GC Pause / ProcessDuration이라는 계산식을 이용해 구합니다.

참고로 ProcessDuration은 관찰된 첫 번째 GC 이벤트와 마지막 GC 이벤트 사이에 소요된 시간입니다.

% CPU Time spent Garbage Collecting(% CPU time in GC)

GC 스레드가 소모한 총 GC와 CPU 수행 시간의 비율을 수치로 나타낸 것입니다. 이 값은 Total GC CPU Time * 100% / Total CPU Time이라는 계산식을 이용해 구합니다.

성능 데이터 수집 시 /KernelEvents=Profile를 지정하지 않으면 이 값은 NaN%으로 표시됩니다.

Max GC Heap Size

성능 데이터를 수집하는 동안 해당 프로세스에서 발생한 최대 관리 힙 크기입니다.

GC Rollup By Generation

각 GC 세대(Gen0/1/2) 별 GC 발생 수, 최대/평균/전체 GC 멈춤 시간 등의 집계 정보가 표시됩니다.

GCs that > 200 msec Events

성능 데이터 수집 동안 200msec 이상의 시간이 소모된 GC 수행 정보가 표시됩니다. 칼럼 헤더에 마우스 호버 시 칼럼에 대한 설명 툴팁이 나타납니다.

LOH allocation pause (due to background GC) > 200msec Events

LOH allocation pause (due to background GC) > 200msec Events의 결과 예
LOH allocation pause (due to background GC) > 200msec Events의 결과 예

Background GC 동안 LOH 할당으로 200msec 이상 중단된 관리 스레드의 정보(중단 시간 포함)가 표시됩니다. 이 정보는 일반적으로 표시되지 않고, BGC 동안 너무 많은 LOH 할당이 발생한 경우에만 표시됩니다. 또한 이 정보는 일반적으로 “LOH 할당을 줄이면 스레드가 차단되지 않는 데 도움이 된다”는 것을 의미합니다.

GCs that were Gen2

성능 데이터 수집 동안 발생한 2세대 GC 수행 정보가 표시됩니다. 칼럼 헤더에 마우스 호버 시 칼럼에 대한 설명 툴팁이 나타납니다.

Individual GC Events

성능 데이터 수집 동안 발생한 모든 GC 수행 정보가 표시됩니다. 칼럼 헤더에 마우스 호버 시 칼럼에 대한 설명 툴팁이 나타납니다.

GCStats Table 칼럼 설명 1

이번에는 앞에서 소개한 GCStats 분석 관련 테이블에서 공통으로 나타나는 주요 칼럼 항목이 무엇을 의미하는지 설명합니다.

주요 칼럼에 대한 설명만 게재합니다. 칼럼 헤더에 마우스 호버 시 칼럼에 대한 설명 툴팁이 나타납니다.

먼저 4가지 칼럼에 대해서 먼저 살펴보겠습니다. 아래 표는 해당 칼럼과 실제 출력한 값의 예입니다.

GC IndexTrigger ReasonGenPause MSec
96AllocSmall0N0.728
97AllocSmall2N650.98
105AllocSmall1N36.057
111AllocSmall2B1.213
113AllocSmall1F136.444
GCStats Table의 전반부 칼럼
GC Index

GC 인덱스를 의미합니다.

Trigger Reason

GC가 발생한 사유를 나타냅니다.

GC는 일반적으로 “메모리 할당, GC.Collect() 메서드 호출, 물리적인 메모리 압박” 등의 사유로 발생합니다.

[Trigger Reason] 칼럼의 값은 아래의 enum 코드와 일치합니다. 아래의 enum 코드는 Microsoft의 PerfView GitHub에서 발췌하여 주석을 추가한 것입니다.

public enum GCReason
{
    AllocSmall = 0x0,           // SOH에 메모리 할당 중 Gen0 Allocation Budget이 초과되어 GC가 발생함
    Induced = 0x1,              // GC.Collect(blocking:true) 메서드 호출로 GC가 발생함
    LowMemory = 0x2,            // 시스템 메모리 부족으로 GC가 발생함
    Empty = 0x3,                // 현재 사용되지 않음
    AllocLarge = 0x4,           // LOH에 메모리 할당 중 Gen3 Allocation Budget이 초과되어 GC가 발생함
    OutOfSpaceSOH = 0x5,        // SOH의 메모리 할당 중 세그먼트의 여유 공간 부족으로 GC가 발생함
    OutOfSpaceLOH = 0x6,        // LOH에 메모리 할당 중 세그먼트의 여유 공간 부족으로 GC가 발생함
    InducedNotForced = 0x7,     // GC.Collect(blocking:false) 메서드 호출로 GC가 발생함
    Internal = 0x8,             // .NET 런타임이 스트레스 테스트 모드로 동작 중에 GC가 발생함
    InducedLowMemory = 0x9,     // 시스템 메모리 부족으로 Blocking GC가 발생함
    InducedCompacting = 0xa,    // GC.Collect(compacting:true) 메서드 호출로 GC가 발생함
    LowMemoryHost = 0xb,        // 현재 사용되지 않음
    PMFullGC = 0xc,             // Provisional Mode가 활성화되어 Full GC가 발생함(시스템 메모리 부족 + 높은 Gen2 단편화)
    LowMemoryHostBlocking = 0xd // 현재 사용되지 않음
}
Gen

발생한 GC 세대를 의미합니다. 결괏값의 알파벳은 다음과 같은 의미입니다.

  • N: Nonconcurrent GC. 2N은 Full blocking GC를 의미합니다.
  • B: Background GC. Gen2 GC만 백그라운드로 수행될 수 있기 때문에 오직 2B만 표시됩니다.
  • F: Foreground GC. 백그라운드 GC 수행 중 임시 세대 GC가 발생한 경우를 의미합니다.
Pause MSec

GC에 의해 중단된 시간입니다.

GCStats Table 칼럼 설명 2

다음으로 5가지 칼럼에 대해서 살펴봅니다. 아래 표는 해당 칼럼과 실제 출력한 값의 예입니다.

Gen0 Alloc MBGen0 Alloc Rate MB/secPeak MBAfter MBPromoted MB
0.25232329400.4329400.4560.252
0.3366329.499400.7686839.3116839.07
2.26924992.866841.5576841.5812.269
0.841166.847122.0217122.0450.84
1.5972871.587123.6187123.561284.838
0.7561092.177124.2937124.3170.756
342.908928.928947.0979289.7649355.565
-23.783-2476.218633.868633.885318.935
313.22733067.718947.0978946.8421234.805
342.8063873515.129289.749289.746342.806
GCStats Table의 후반부 칼럼
Gen0 Alloc MB

이전 GC 발생 이후 현재 GC 발생 전까지 Gen0에 할당된 메모리 크기입니다.

Gen0 Alloc Rate MB/sec

이전 GC 발생 이후 현재 GC 발생 전까지 Gen0에 할당된 초당 메모리 크기 비율 수치입니다.

Peak MB

GC 수행 전의 전체 힙 크기입니다. 다음 사항을 참고하기 바랍니다.

  • 단편화된 메모리 영역의 크기도 포함됩니다.
  • 따라서 After MB(이전 GC)와 Gen0 Alloc MB(현재 GC)의 합은 Peak MB(현재 GC) 값과 일치하지 않을 수 있습니다.
After MB

GC 수행 후의 전체 힙 크기입니다. 다음 사항을 참고하기 바랍니다.

  • 단편화된 메모리 영역의 크기도 포함됩니다.
  • 계산식: Gen0 MB + Gen1 MB + Gen2 MB + LOH MB
Promoted MB

GC 수행 중 상위 세대로 승급(Promotion)된 메모리 크기입니다.

Condemned reasons for GCs

GC 세대가 결정된(판정된) 사유에 대한 정보가 표시됩니다. 칼럼 헤더 각각에 마우스 호버 시 해당 칼럼에 대한 설명 툴팁이 나타납니다.

‘GC가 발생된 사유(Trigger reason)’는 왜 GC가 시작하는지에 대한 것입니다. GC가 시작되는 가장 일반적인 이유는 SOH 메모리 할당이기 때문에 GC는 Gen0 GC로 시작합니다. 이렇게 GC가 시작된 이후에 실제로 수집할 세대를 결정합니다. 세대 결정은 Gen0 GC로 유지되거나 Gen1 또는 Gen2 GC로 단계 상승(Escalation)이 될 수 있습니다 – 이러한 세대 결정은 GC 수행 중 가장 먼저 실행되는 로직입니다. 그리고 높은 세대 GC로 단계 상승(Escalation)이 발생하는 요인을 ‘GC 세대가 결정된(판정된) 사유’라고 부릅니다. 따라서 GC가 발생된 사유는 한 가지뿐이지만 GC 세대가 결정된 사유는 여러 개일 수 있습니다.

Condemned Reasons Table 칼럼 설명

Condemned Reasons Table에는 GCStats 분석 관련 공통 테이블 못지않은 여러 가지 칼럼 항목이 있습니다. 주요 칼럼 항목이 무엇을 의미하는지 살펴보겠습니다.

Heap Index

GC를 발생시킨 첫 번째 힙의 인덱스입니다. 서버 GC 모드에서만 표시됩니다.

Initial Requested Generation

GC가 발생된 세대 번호입니다.

Final Generation

GC가 수집된 최종 세대 번호입니다.

Generation Budget Exceeded

Allocation Budget이 초과된 가장 높은 세대 번호입니다.

Time Tuning

GC 타임 초과가 발생한 세대 번호입니다. 다음 사항을 참고하기 바랍니다.

  • 일반적으로 10초 동안 GC가 발생하지 않으면 강제로 GC가 수행됩니다.
  • 이 값은 GC 모드가 Workstation이고, GCLatencyMode가 Interactive 또는 SustainedLowLatency 값일 때만 유효합니다.
Induced

GC.Collect() 메서드 호출로 GC가 발생하였는지 유무를 나타냅니다. 주요 결괏값 항목은 다음과 같습니다.

  • Blocking: GC.Collect(blocking:true) 메서드 호출로 GC가 발생함
  • NotForced: GC.Collect(blocking:false) 메서드 호출로 GC가 발생함
  • 0: GC.Collect() 메서드 호출로 GC가 발생한 것이 아님
Ephemeral Low

임시 세대의 여유 공간 부족으로 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다. 이 경우 최소 Gen1 GC가 수행됩니다.

Expand Heap

임시 세대의 여유 공간이 부족하여 세그먼트가 확장을 위해 Gen2 GC가 발생하였는지 유무 (1 또는 0)를 나타냅니다.

Fragmented Ephemeral

임시 세대가 너무 단편화되어서 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다.

Low Ephemeral Fragmented Gen2

임시 세대의 여유 공간이 부족하고 Gen2의 단편화 공간이 충분한 경우, Gen2 컴팩션을 위해 Gen2 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다. Gen2 세그먼트 확장 대신 Gen2 컴팩션으로 충분한 여유 공간이 확보될 수 있는 경우입니다.

Fragmented Gen2

Gen2가 너무 단편화되어서 Gen2 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다.

High Memory

시스템 메모리가 부족하여 Gen2 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다.

Compacting Full GC

시스템 메모리가 부족하고 Gen2가 너무 단편화되어서 Gen2 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다. OOM(Out Of Memory)를 던지기 전에 GC가 발생한 경우입니다.

Small Heap

힙 크기가 너무 작아서 BGC(Background GC) 대신 Gen2 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다.

Ephemeral Before BGC

BGC(Background GC) 수행 전에 임시 세대 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다.

Internal Tuning

.NET 런타임 내부 동작(Card Table 부족)으로 인해 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다. 이때 Trigger ReasonInternal인 경우와 무관합니다.

Max Generation Budget

시스템 메모리가 부족하고 최대 세대(2세대)의 Allocation Budget이 일정 수준 이상으로 소모되어서 Gen2 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다. 최대 세대(2세대)의 Allocation Budget이 10% 이상 소모된 경우입니다.

Avoid Unproductive Full GC

Full Compacting GC 수행 직후에 Gen2 GC 대신 Gen1 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다. Full Compacting GC 직후에 수행하는 Gen2 GC는 비효율적이기 때문에 Gen2에서 Gen1으로 GC 단계가 하강된 경우입니다.

Provisional Mode Induced

Provisional Mode가 활성화되고, Gen2 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다. 시스템 메모리 부족과 높은 Gen2 단편화로 인해 Provisional Mode가 활성화되고, Gen1 GC가 Gen2의 크기를 증가시켜서 Full GC가 발생한 경우입니다.

Provisional Mode LOH alloc

Provisional Mode가 활성화되고, Gen2 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다. 시스템 메모리 부족과 높은 Gen2 단편화로 인해 Provisional Mode가 활성화되고, LOH에 메모리 할당 중에 GC가 발생한 경우입니다.

Provision Mode

Provisional Mode가 활성화되고, Gen1 GC가 발생하였는지 유무 (1 또는 0)를 나타냅니다. 시스템 메모리 부족과 높은 Gen2 단편화로 인해 Provisional Mode가 활성화되지만, Gen2 크기 증가 전까지 정상적으로 Gen1 GC가 수행 가능한 경우입니다.

Compacting Full under HardLimit

HardLimit가 설정된 상태에서 OOM(Out Of Memory)을 던지기 전에 Gen2 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다. GCHeapHardLimit, GCHeapHardLimitSOH/LOH/POH 등의 HardLimit가 설정된 경우입니다.

LOH Frag HardLimit

HardLimit가 설정된 상태에서 LOH 단편화가 일정 수준 이상이어서 Gen2 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다. LOH 단편화 크기가 HardLimit 크기의 1/8보다 크거나 같을 경우입니다.

LOH Reclaim HardLimit

HardLimit가 설정된 상태에서 LOH의 예상 메모리 회수 크기가 일정 수준 이상이어서 Gen2 GC가 발생하였는지 유무(1 또는 0)를 나타냅니다. LOH의 예상 메모리 회수 크기가 HardLimit 크기의 1/8보다 크거나 같을 경우입니다.

참고 사항

Provisional Mode는 Full Blocking GC 수행 중 시스템 메모리 부족과 높은 Gen2 단편화가 탐지되었을 때 활성화됩니다. Provisional Mode는 결정된(판정된) GC 세대 수집이 효율적이지 않다고 판단될 경우 다른 GC 세대를 선택합니다. 즉, 이번 GC 수행 중 Provisional Mode가 활성화되었다면 다음번 GC 수행 시 수집할 GC 세대가 결정된 이후에 상황에 따라서 수집할 GC 세대가 변경될 수 있습니다. 더 자세한 사항은 Provisional Mode를 확인하기 바랍니다.

GC Heap Alloc Ignore Free (Coarse Sampling) Stacks 분석

GC Heap Alloc Ignore Free Stacks 분석은 할당된 객체의 종류(Type), 객체가 할당된 시점의 콜스택 등의 분석이 가능한 항목입니다.
[Memory] 그룹 항목 하단에서 [GC Heap Alloc Ignore Free (Coarse Sampling) Stacks] 항목을 더블 클릭합니다. 프로세스 선택 창이 실행됩니다.

PerfView는 OS상에 실행 중인 모든 프로세스로부터 데이터를 수집하기 때문에 같은 분석 항목이 프로세스별로 표시됩니다.

분석 대상 프로세스를 확인하고 해당 항목을 더블 클릭하거나, 항목을 선택 후 [OK] 버튼을 클릭합니다. 대상 프로세스에 대한 다음과 같은 객체 할당 정보가 표시됩니다.

Stack Viewer 화면 구성 요소 설명

이 글에서는 Stack Viewer의 주요 화면 구성 요소에 대한 설명만 게재합니다. 또한 구성 요소 중 라벨 또는 물음표를 클릭하면 해당 요소에 대한 설명 창이 실행되니 해당 내용도 참고하기 바랍니다.

그룹 제어바

그룹 제어바는 다음과 같은 요소가 있습니다.

  • Start / End: 특정 타임 구간을 MSec 단위로 지정할 수 있습니다.
  • Find: 탭 하위 칼럼 중 [Name] 칼럼의 문자열을 검색할 수 있습니다.
  • GroupPats: 객체 할당 정보를 의미론적으로 그룹화할 수 있습니다. 일반적으로 [no grouping] 항목을 선택하거나 문자열을 모두 지워서 그룹화를 해제합니다. 또한 [Callers] 탭에서 상세한 콜스택 정보를 보기 위해서는 반드시 그룹화를 해제해야 합니다.
뷰 선택 탭

뷰 선택 탭에는 다음과 같은 요소가 있습니다.

  • By Name: 할당된 객체의 이름으로 항목들이 그룹화되어 표시됩니다. 기본적으로 Exc 칼럼값으로 정렬되어 표시됩니다. 그리드 뷰에서 객체 이름 항목을 더블 클릭하면, [Callers] 탭이 활성화되고 해당 객체의 할당이 발생한 콜스택 정보가 표시됩니다.
  • CallTree: ROOT 객체로부터 객체가 할당된 콜트리 정보가 표시됩니다. 참고로 ROOT는 프로세스를 의미합니다.
  • Callers: [CallTree]와 반대로 객체 할당이 발생한 콜스택 정보가 표시됩니다. [By Name] 탭을 선택 후 그리드 뷰에서 객체 이름 항목을 더블 클릭하여야 해당 객체의 할당이 발생한 콜스택 정보를 확인할 수 있습니다.
그리드 뷰 칼럼

그리드 뷰 칼럼에는 다음과 같은 요소가 있습니다.

  • Name: 할당된 객체의 이름입니다.
  • Exc % / Exc: 객체가 독점적으로 소모한 CPU 시간 값입니다.
  • Inc % / Inc: 객체가 독점적으로 소모한 CPU 시간과 객체 할당 과정(콜스택)에서 직·간접적으로 소모한 CPU 시간 값의 합입니다. ROOT(프로세스) 객체는 모든 호출에 직·간접적으로 연관이 되어있기 때문에 Inc % 값이 항상 100%입니다.

심볼 정보 불러오기

그리드 뷰 영역에서 우클릭 후 컨텍스트 메뉴에서 [Lookup Warm Symbols] 항목을 선택하면, Stack 표시를 위한 심볼 정보를 불러옵니다.

심볼 정보를 불러오지 않을 경우, Stack Viewer의 콜스택 정보에서 정확한 호출 메서드 정보를 확인할 수 없다는 점에 주의하기 바랍니다.

Pinning At GC Time Stacks 분석

Pinning At GC Time Stacks 분석은 GC 발생 시점에서의 Pinning 객체 수, Pinned 객체가 할당된 시점의 콜스택 등의 분석이 가능한 항목입니다.

[Advanced] 그룹 항목 하단에서 [Pinning At GC Time Stacks] 항목을 더블 클릭합니다. 별도의 창이 실행되면서 최상단에 아래와 같은 항목이 표시됩니다.

GC Mark Phase 단계에서 Pinning 상태의 객체에 대한 콜스택 정보를 표시합니다.

참고하면 좋은 성능 데이터 활용 및 수집 방법

PerfView로 수집된 성능 데이터 활용하기

PerfView로 수집된 성능 데이터 파일(PerfViewData.etl.zip)은 Windows Performance Analyzer에서도 사용 가능합니다(따라서 Windows Performance Recorder를 이용하여 별도로 성능 데이터를 수집할 필요가 없습니다).

WPA는 매우 유연한 UI와 광범위한 그래프 기능 및 전체 텍스트 검색 기능이 있는 데이터 테이블을 결합한 강력한 분석 도구입니다.

리눅스에서 GC 성능 데이터 수집

dotnet-trace를 이용하여 리눅스에서도 GC 성능 데이터를 수집할 수 있습니다.

제한 사항: 리눅스에서 수집된 성능 데이터로는 객체 할당 콜스택 정보를 확인할 수 없습니다. 즉, [PerfView] → [Memory] → [GC Heap Alloc Ignore Free (Coarse Sampling) Stacks] 항목에서 객체 할당이 발생한 콜스택 정보를 확인할 수 없습니다.

실행 명령어는 다음과 같으니 참고하기 바랍니다.

$ dotnet-trace collect --profile gc-verbose --buffersize 3000 --duration 00:01:40 -n gc-stress-test

GC의 효율성을 가장 잘 살리는 방법은 GC 프로파일링입니다.

모던 프로그래밍에서 GC를 사용하지 않는다는 건 프로그래머의 생산성을 높이려고 만든 기능을 마다하는 일이죠. 그만큼 성능 관리에 고려해야 할 요소가 늘어난다는 의미입니다. 그렇다고 GC에 무작정 성능 관리를 맡기는 것도 올바른 정답은 아닐 것으로 생각합니다. GC의 사각지대는 넓은 편이기 때문이죠.

이럴 때 두 마리 토끼를 모두 잡는 방법의 하나가 바로 지금 설명한 GC 프로파일링이 아닐까 합니다. 혹시 아직까지도 .NET GC 성능 최적화에 고민이 깊다면 이 글이 도움이 되길 바랍니다. 비효율적인 GC를 순식간에 찾고 빠르게 퇴근하기를 기원합니다.