본문 바로가기

C#

C# Stopwatch와 PerformanceCounter 비교 분석

코드 성능을 측정할 때 무엇을 써야 할지 고민되기 쉽습니다. Stopwatch는 코드 블록의 경과 시간 측정에 최적화되어 있고, PerformanceCounter는 Windows 시스템/프로세스 수준 지표 관찰에 좋습니다. 이 글에서는 두 도구의 핵심 차이, 정확도/오버헤드, 올바른 사용 시나리오와 예제를 간략히 정리합니다.

1. 핵심 요약

Stopwatch: 경과 시간(벽시계 시간) 측정에 특화된 경량 타이머입니다. 고해상도 타이머(QueryPerformanceCounter 등)를 사용하며, 마이크로벤치마크와 코드 경로 비교에 적합합니다.

PerformanceCounter: Windows 퍼포먼스 카운터를 읽어 시스템/프로세스 지표(CPU, 메모리 등)를 샘플링합니다. 코드 블록 하나의 소요 시간을 재기보다는, 실행 중인 앱의 리소스 사용 추이를 관찰할 때 적합합니다.

2. Stopwatch: 언제, 왜 쓰나

- 마이크로벤치마크, 특정 메서드의 경과 시간 비교에 사용합니다.

- Stopwatch.IsHighResolution와 Stopwatch.Frequency로 타이머 해상도를 확인할 수 있습니다.

- 장점: 경량, 정확도 높음(단일 스레드에서 모노토닉), 사용 간단. 단점: CPU 시간과 I/O 대기 시간을 구분하지 못함(벽시계 기준).

using System;
using System.Diagnostics;
using System.Threading;

class Program
{
    static void Main()
    {
        // 타이머 해상도 확인
        Console.WriteLine($"HighResolution: {Stopwatch.IsHighResolution}, Freq: {Stopwatch.Frequency} ticks/sec");

        // 단순 측정
        var sw = Stopwatch.StartNew();
        DoWork();
        sw.Stop();
        Console.WriteLine($"Elapsed: {sw.Elapsed.TotalMilliseconds:F3} ms");

        // 반복 측정(평균) - JIT 워밍업 후
        DoWork();
        int iterations = 1000;
        sw.Restart();
        for (int i = 0; i < iterations; i++) DoWork();
        sw.Stop();
        Console.WriteLine($"Avg: {sw.Elapsed.TotalMilliseconds / iterations:F6} ms/op");
    }

    static void DoWork()
    {
        // 측정 대상 코드(예시)
        Thread.SpinWait(100_000);
    }
}

3. PerformanceCounter: 언제, 왜 쓰나

- Windows 전용으로, OS가 제공하는 카운터를 샘플링해 CPU 사용률, 메모리, GC 등 런타임/프로세스 지표를 관찰합니다.

- 장점: 시스템/프로세스 수준 인사이트, 추세 관찰에 유리. 단점: 비교적 높은 오버헤드, 샘플 간격 필요, 인스턴스 이름 처리와 권한 이슈, Windows 전용.

- .NET 6+에서도 Windows에서만 지원되며, 컨테이너/일부 환경에서는 카운터가 없을 수 있습니다.

using System;
using System.Diagnostics;
using System.Threading;

class Program
{
    static void Main()
    {
        if (!OperatingSystem.IsWindows())
        {
            Console.WriteLine("PerformanceCounter는 Windows에서만 지원됩니다.");
            return;
        }

        string instance = GetProcessInstanceName(Process.GetCurrentProcess().Id);

        using var cpu = new PerformanceCounter("Process", "% Processor Time", instance, true);
        using var ws  = new PerformanceCounter("Process", "Working Set - Private", instance, true);

        // 첫 호출은 의미 없는 값일 수 있으므로 간격을 두고 두 번 읽습니다.
        cpu.NextValue();
        Thread.Sleep(500);

        float cpuPct = cpu.NextValue() / Environment.ProcessorCount; // 논리 코어 수로 정규화
        float privateMb = ws.NextValue() / (1024 * 1024);

        Console.WriteLine($"CPU: {cpuPct:F1}%  Private WS: {privateMb:F1} MB");
    }

    static string GetProcessInstanceName(int pid)
    {
        var cat = new PerformanceCounterCategory("Process");
        foreach (var name in cat.GetInstanceNames())
        {
            using var cnt = new PerformanceCounter("Process", "ID Process", name, true);
            if ((int)cnt.RawValue == pid) return name; // 같은 이름의 다중 인스턴스(#1 등) 대응
        }
        return Process.GetCurrentProcess().ProcessName;
    }
}

4. 정확도와 오버헤드 비교

- 정확도: Stopwatch는 코드 블록의 경과 시간을 고해상도로 제공합니다. PerformanceCounter는 샘플링 기반이라 순간 측정보다 평균/추세 해석에 적합합니다.

- 오버헤드: Stopwatch는 매우 낮습니다. PerformanceCounter는 카테고리/카운터 조회 비용과 보안 컨텍스트에 따라 더 큽니다.

- 범위: Stopwatch는 코드 단위, PerformanceCounter는 프로세스/시스템 단위입니다.

5. CPU 시간과 벽시계 시간 함께 보기(대안)

코드가 실제로 CPU를 얼마나 썼는지 확인하려면 프로세스의 CPU 누적 시간을 함께 측정하면 유용합니다.

using System;
using System.Diagnostics;

class Program
{
    static void Main()
    {
        var process = Process.GetCurrentProcess();

        var cpuBefore = process.TotalProcessorTime;
        var wall = Stopwatch.StartNew();
        DoCpuBound();
        wall.Stop();
        process.Refresh();
        var cpuAfter = process.TotalProcessorTime;

        Console.WriteLine($"Wall: {wall.Elapsed.TotalMilliseconds:F3} ms");
        Console.WriteLine($"CPU : {(cpuAfter - cpuBefore).TotalMilliseconds:F3} ms");
    }

    static void DoCpuBound()
    {
        double s = 0;
        for (int i = 0; i < 10_000_000; i++) s += Math.Sqrt(i);
        GC.KeepAlive(s);
    }
}

6. 언제 무엇을 쓸까

- 코드 블록/알고리즘 비교: Stopwatch 권장.

- 운영 중 CPU/메모리 추세 관찰, 대시보드/경보: PerformanceCounter(Windows) 또는 EventCounters/dotnet-counters.

- 벽시계 vs CPU 사용량을 함께 보고 병목 파악: Stopwatch + Process.TotalProcessorTime 조합.

7. 흔한 함정과 팁

- DateTime.Now로 측정하지 마세요. 모노토닉 보장되지 않습니다. Stopwatch를 사용합니다.

- JIT/캐시 효과를 줄이려면 워밍업 후 반복 측정하고, Release 빌드/디버거 분리 상태에서 실행합니다.

- PerformanceCounter는 첫 읽기값 무시, 샘플 간격 확보가 필요합니다. 일부 카운터는 관리자 권한이나 카테고리 설치가 필요합니다.

- 동일한 프로세스 이름의 다중 인스턴스(#1 등) 처리에 유의하세요.

8. 추천 대안 도구

- 마이크로벤치: BenchmarkDotNet(강력한 통계/워크로드 격리).

- 런타임 지표: dotnet-counters, EventCounters, PerfView/Windows Performance Recorder.

9. 결론

Stopwatch는 가볍고 정밀한 코드 블록 경과 시간 측정에 최적입니다. PerformanceCounter는 Windows에서 시스템/프로세스 지표를 샘플링해 추세를 보는 데 유용합니다. 목적이 다른 도구이므로, 코드 경로 비교엔 Stopwatch, 운영 지표 관찰엔 PerformanceCounter(또는 현대적인 이벤트/메트릭 도구)를 선택하는 것이 좋습니다.