BenchmarkDotNet으로 C# 성능 측정
BenchmarkDotNet은 .NET 생태계의 표준 마이크로벤치마크 라이브러리입니다. 워밍업, 통계 분석, JIT 최적화 방지를 자동으로 처리하므로 Stopwatch보다 훨씬 신뢰할 수 있는 측정치를 제공합니다. 메모리 할당량까지 추적해 GC 최적화 효과를 정량화할 수 있습니다.
1. 기본 설정
섹션 제목: “1. 기본 설정”# NuGet 설치dotnet add package BenchmarkDotNet
# Release 모드로 실행 (필수)dotnet run -c Releaseusing BenchmarkDotNet.Attributes;using BenchmarkDotNet.Running;
// 진입점BenchmarkRunner.Run<StringBenchmarks>();2. 기본 벤치마크
섹션 제목: “2. 기본 벤치마크”[MemoryDiagnoser] // 메모리 할당 추적[SimpleJob(RuntimeMoniker.Net80)]public class StringBenchmarks{ private const string Input = "Hello, BenchmarkDotNet!";
[Benchmark(Baseline = true)] public string StringConcat() { string result = ""; for (int i = 0; i < 100; i++) result += Input[i % Input.Length]; return result; }
[Benchmark] public string StringBuilder() { var sb = new System.Text.StringBuilder(); for (int i = 0; i < 100; i++) sb.Append(Input[i % Input.Length]); return sb.ToString(); }
[Benchmark] public string StringCreate() { return string.Create(100, Input, static (span, src) => { for (int i = 0; i < span.Length; i++) span[i] = src[i % src.Length]; }); }}3. 결과 해석
섹션 제목: “3. 결과 해석”| Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated ||-------------- |----------:|---------:|---------:|------:|-------:|----------:|| StringConcat | 4,521 ns | 23.14 ns | 21.65 ns | 1.00 | 3.4180 | 5,712 B || StringBuilder | 312 ns | 2.81 ns | 2.63 ns | 0.07 | 0.1535 | 256 B || StringCreate | 198 ns | 1.44 ns | 1.28 ns | 0.04 | 0.0153 | 26 B |
- Mean: 평균 실행 시간 (ns/μs/ms)- Ratio: Baseline 대비 비율 (낮을수록 빠름)- Gen0: GC 0세대 수집 빈도 (1000회당)- Allocated: 단일 실행당 힙 할당 바이트4. Params — 매개변수 변화 테스트
섹션 제목: “4. Params — 매개변수 변화 테스트”[MemoryDiagnoser]public class CollectionBenchmarks{ [Params(10, 100, 1000, 10_000)] public int N;
private int[] _data = null!;
[GlobalSetup] public void Setup() => _data = Enumerable.Range(0, N).ToArray();
[Benchmark(Baseline = true)] public int LinqSum() => _data.Sum();
[Benchmark] public int LoopSum() { int sum = 0; foreach (int x in _data) sum += x; return sum; }
[Benchmark] public int SpanSum() { ReadOnlySpan<int> span = _data; int sum = 0; for (int i = 0; i < span.Length; i++) sum += span[i]; return sum; }}5. Job 설정 — 여러 런타임 비교
섹션 제목: “5. Job 설정 — 여러 런타임 비교”[SimpleJob(RuntimeMoniker.Net80)][SimpleJob(RuntimeMoniker.Net60)][SimpleJob(RuntimeMoniker.NativeAot80)] // NativeAOT[MemoryDiagnoser][HtmlExporter] // HTML 보고서[CsvExporter] // CSV 내보내기public class CrossRuntimeBenchmarks{ [Benchmark] public double MathSqrt() => Math.Sqrt(12345.6789);}6. 커스텀 설정
섹션 제목: “6. 커스텀 설정”public class Config : ManualConfig{ public Config() { AddJob(Job.Default .WithRuntime(CoreRuntime.Core80) .WithWarmupCount(3) .WithIterationCount(10) .WithInvocationCount(1024));
AddDiagnoser(MemoryDiagnoser.Default); AddDiagnoser(new DisassemblyDiagnoser( new DisassemblyDiagnoserConfig(maxDepth: 2))); AddExporter(MarkdownExporter.GitHub); }}
[Config(typeof(Config))]public class MyBenchmarks { }7. 흔한 실수 방지
섹션 제목: “7. 흔한 실수 방지”public class CorrectBenchmarks{ private List<int> _list = null!;
[GlobalSetup] public void Setup() => _list = Enumerable.Range(0, 1000).ToList();
// ✅ 결과를 반환해 데드 코드 제거 방지 [Benchmark] public int SumCorrect() => _list.Sum();
// ❌ 결과를 버리면 JIT이 코드를 제거할 수 있음 [Benchmark] public void SumWrong() { _list.Sum(); }
// ✅ 여러 결과: Consumer 사용 [Benchmark] public void MultipleOps() { var consumer = new BenchmarkDotNet.Engines.Consumer(); consumer.Consume(_list.Sum()); consumer.Consume(_list.Count); }}8. DisassemblyDiagnoser — JIT 어셈블리 확인
섹션 제목: “8. DisassemblyDiagnoser — JIT 어셈블리 확인”[DisassemblyDiagnoser(maxDepth: 3, printSource: true)][SimpleJob(RuntimeMoniker.Net80)]public class JitBenchmarks{ private int[] _data = Enumerable.Range(0, 1000).ToArray();
[Benchmark] public int SumSpan() { ReadOnlySpan<int> span = _data; int sum = 0; for (int i = 0; i < span.Length; i++) sum += span[i]; return sum; }}
// 결과에 ASM 코드 출력:// ; SumBenchmarks.SumSpan()// ; .NET 8.0.0 (8.0.0), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI// ...vmovdqu ymm0, [rcx+rdi*4+16]// ...vpaddd ymm1, ymm1, ymm0// → AVX-512 SIMD 자동 벡터화 확인 가능9. EventPipeProfiler — GC/JIT 이벤트 추적
섹션 제목: “9. EventPipeProfiler — GC/JIT 이벤트 추적”using BenchmarkDotNet.Diagnosers;
[EventPipeProfiler(EventPipeProfile.GcVerbose)][MemoryDiagnoser]public class GcBenchmarks{ [Benchmark] public List<int> AllocateList() => Enumerable.Range(0, 10000).ToList();
[Benchmark] public int[] AllocateArray() => Enumerable.Range(0, 10000).ToArray();}
// 결과 파일: BenchmarkDotNet.Artifacts/results/*.nettrace// dotnet-trace view 로 분석 가능10. 벤치마크 결과 비교 (CI 통합)
섹션 제목: “10. 벤치마크 결과 비교 (CI 통합)”# 결과를 JSON으로 내보내기dotnet run -c Release -- --exporters json
# 이전 결과와 비교 (임계값 설정)# BenchmarkDotNet.Artifacts/results/*.json 비교
# GitHub Actions 예시# - uses: dotnet/benchmarkdotnet-action@v1# with:# project: benchmarks/MyBenchmarks.csproj# threshold: 5% # 5% 이상 회귀 시 실패BenchmarkDotNet은 [MemoryDiagnoser]와 [Params]를 함께 사용할 때 가장 유용합니다. 결과에서 Allocated 열을 주목해 할당 제로화(zero-alloc) 최적화 효과를 검증하고, [Baseline = true]로 개선 비율을 명확히 표시하세요. [DisassemblyDiagnoser]로 JIT가 SIMD 명령어를 생성하는지 확인하고, CI 파이프라인에 벤치마크를 통합해 성능 회귀를 자동으로 감지하세요.