|
| 1 | +--- |
| 2 | +name: performance-benchmark |
| 3 | +description: Generate and run ad hoc performance benchmarks to validate code changes. Use this when asked to benchmark, profile, or validate the performance impact of a code change in dotnet/runtime. |
| 4 | +--- |
| 5 | + |
| 6 | +# Ad Hoc Performance Benchmarking |
| 7 | + |
| 8 | +When you need to validate the performance impact of a code change, follow this process to write a BenchmarkDotNet benchmark and trigger EgorBot to run it. |
| 9 | + |
| 10 | +## Step 1: Write the Benchmark |
| 11 | + |
| 12 | +Create a BenchmarkDotNet benchmark that tests the specific operation being changed. Follow these guidelines: |
| 13 | + |
| 14 | +### Benchmark Structure |
| 15 | + |
| 16 | +```csharp |
| 17 | +using BenchmarkDotNet.Attributes; |
| 18 | +using BenchmarkDotNet.Running; |
| 19 | + |
| 20 | +BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args); |
| 21 | + |
| 22 | +public class Bench |
| 23 | +{ |
| 24 | + // Add setup/cleanup if needed |
| 25 | + [GlobalSetup] |
| 26 | + public void Setup() |
| 27 | + { |
| 28 | + // Initialize test data |
| 29 | + } |
| 30 | + |
| 31 | + [Benchmark] |
| 32 | + public void MyOperation() |
| 33 | + { |
| 34 | + // Test the operation |
| 35 | + } |
| 36 | +} |
| 37 | +``` |
| 38 | + |
| 39 | +### Best Practices |
| 40 | + |
| 41 | +For comprehensive guidance, see the [Microbenchmark Design Guidelines](https://github.com/dotnet/performance/blob/main/docs/microbenchmark-design-guidelines.md). |
| 42 | + |
| 43 | +Key principles: |
| 44 | + |
| 45 | +- **Move initialization to `[GlobalSetup]`**: Separate setup logic from the measured code to avoid measuring allocation/initialization overhead |
| 46 | +- **Return values** from benchmark methods to prevent dead code elimination |
| 47 | +- **Avoid loops**: BenchmarkDotNet invokes the benchmark many times automatically; adding manual loops distorts measurements |
| 48 | +- **No side effects**: Benchmarks should be pure and produce consistent results |
| 49 | +- **Focus on common cases**: Benchmark hot paths and typical usage, not edge cases or error paths |
| 50 | +- **Use consistent input data**: Always use the same test data for reproducible comparisons |
| 51 | +- **Avoid `[DisassemblyDiagnoser]`**: It causes crashes on Linux. Use `--envvars DOTNET_JitDisasm:MethodName` instead |
| 52 | +- **Benchmark class requirements**: Must be `public`, not `sealed`, not `static`, and must be a `class` (not struct) |
| 53 | + |
| 54 | +### Example: String Operation Benchmark |
| 55 | + |
| 56 | +```csharp |
| 57 | +using BenchmarkDotNet.Attributes; |
| 58 | +using BenchmarkDotNet.Running; |
| 59 | + |
| 60 | +BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args); |
| 61 | + |
| 62 | +[MemoryDiagnoser] |
| 63 | +public class Bench |
| 64 | +{ |
| 65 | + private string _testString = default!; |
| 66 | + |
| 67 | + [Params(10, 100, 1000)] |
| 68 | + public int Length { get; set; } |
| 69 | + |
| 70 | + [GlobalSetup] |
| 71 | + public void Setup() |
| 72 | + { |
| 73 | + _testString = new string('a', Length); |
| 74 | + } |
| 75 | + |
| 76 | + [Benchmark] |
| 77 | + public int StringOperation() |
| 78 | + { |
| 79 | + return _testString.IndexOf('z'); |
| 80 | + } |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +### Example: Collection Operation Benchmark |
| 85 | + |
| 86 | +```csharp |
| 87 | +using System.Linq; |
| 88 | +using BenchmarkDotNet.Attributes; |
| 89 | +using BenchmarkDotNet.Running; |
| 90 | + |
| 91 | +BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args); |
| 92 | + |
| 93 | +[MemoryDiagnoser] |
| 94 | +public class Bench |
| 95 | +{ |
| 96 | + private int[] _array = default!; |
| 97 | + private List<int> _list = default!; |
| 98 | + |
| 99 | + [Params(100, 1000, 10000)] |
| 100 | + public int Count { get; set; } |
| 101 | + |
| 102 | + [GlobalSetup] |
| 103 | + public void Setup() |
| 104 | + { |
| 105 | + _array = Enumerable.Range(0, Count).ToArray(); |
| 106 | + _list = _array.ToList(); |
| 107 | + } |
| 108 | + |
| 109 | + [Benchmark] |
| 110 | + public bool AnyArray() => _array.Any(); |
| 111 | + |
| 112 | + [Benchmark] |
| 113 | + public bool AnyList() => _list.Any(); |
| 114 | + |
| 115 | + [Benchmark] |
| 116 | + public int SumArray() => _array.Sum(); |
| 117 | + |
| 118 | + [Benchmark] |
| 119 | + public int SumList() => _list.Sum(); |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +## Step 2: Post the EgorBot Comment |
| 124 | + |
| 125 | +Post a comment on the PR to trigger EgorBot with your benchmark. The general format is: |
| 126 | + |
| 127 | +``` |
| 128 | +@EgorBot [target flags] [options] [BenchmarkDotNet args] |
| 129 | +
|
| 130 | +```cs |
| 131 | +// Your benchmark code here |
| 132 | +``` |
| 133 | +``` |
| 134 | +
|
| 135 | +### Target Flags (Required - Choose at Least One) |
| 136 | +
|
| 137 | +| Flag | Architecture | Description | |
| 138 | +|------|--------------|-------------| |
| 139 | +| `-x64` or `-amd` | x64 | Linux Azure Genoa (AMD EPYC) - default x64 target | |
| 140 | +| `-arm` | ARM64 | Linux Azure Cobalt100 (Neoverse-N2) | |
| 141 | +| `-intel` | x64 | Azure Cascade Lake (more flaky due to JCC Erratum and loop alignment sensitivity) | |
| 142 | +| `-windows_x64` | x64 | Windows x64 (when Windows-specific testing is needed) | |
| 143 | +
|
| 144 | +**Choosing targets:** |
| 145 | +
|
| 146 | +- **Default for most changes**: Use `-x64` for quick verification of non-architecture/non-OS specific changes |
| 147 | +- **Default when ARM might differ**: Use `-x64 -arm` if there's any suspicion the change might behave differently on ARM |
| 148 | +- **Windows-specific changes**: Use `-windows_x64` when Windows behavior needs testing |
| 149 | +- **Noisy results suspected**: Use `-arm -intel -amd` to get results from multiple x64 CPUs (note: `-intel` targets are more flaky) |
| 150 | +
|
| 151 | +### Common Options |
| 152 | +
|
| 153 | +| Option | Description | |
| 154 | +|--------|-------------| |
| 155 | +| `-profiler` | Collect flamegraph/hot assembly using perf record | |
| 156 | +| `--envvars KEY:VALUE` | Set environment variables (e.g., `DOTNET_JitDisasm:MethodName`) | |
| 157 | +| `-commit <hash>` | Run against a specific commit | |
| 158 | +| `-commit <hash1> vs <hash2>` | Compare two commits | |
| 159 | +| `-commit <hash> vs previous` | Compare commit with its parent | |
| 160 | +
|
| 161 | +### Example: Basic PR Benchmark |
| 162 | +
|
| 163 | +To benchmark the current PR changes against the base branch: |
| 164 | +
|
| 165 | +``` |
| 166 | +@EgorBot -x64 -arm |
| 167 | + |
| 168 | +```cs |
| 169 | +using BenchmarkDotNet.Attributes; |
| 170 | +using BenchmarkDotNet.Running; |
| 171 | + |
| 172 | +BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args); |
| 173 | + |
| 174 | +[MemoryDiagnoser] |
| 175 | +public class Bench |
| 176 | +{ |
| 177 | + [Benchmark] |
| 178 | + public int MyOperation() |
| 179 | + { |
| 180 | + // Your benchmark code |
| 181 | + return 42; |
| 182 | + } |
| 183 | +} |
| 184 | +``` |
| 185 | +``` |
| 186 | +
|
| 187 | +### Example: Benchmark with Profiling and Disassembly |
| 188 | +
|
| 189 | +``` |
| 190 | +@EgorBot -x64 -profiler --envvars DOTNET_JitDisasm:SumArray |
| 191 | + |
| 192 | +```cs |
| 193 | +using System.Linq; |
| 194 | +using BenchmarkDotNet.Attributes; |
| 195 | +using BenchmarkDotNet.Running; |
| 196 | + |
| 197 | +BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args); |
| 198 | + |
| 199 | +public class Bench |
| 200 | +{ |
| 201 | + private int[] _data = Enumerable.Range(0, 1000).ToArray(); |
| 202 | + |
| 203 | + [Benchmark] |
| 204 | + public int SumArray() => _data.Sum(); |
| 205 | +} |
| 206 | +``` |
| 207 | +``` |
| 208 | +
|
| 209 | +### Example: Compare Two Commits |
| 210 | +
|
| 211 | +``` |
| 212 | +@EgorBot -amd -commit abc1234 vs def5678 |
| 213 | + |
| 214 | +```cs |
| 215 | +using BenchmarkDotNet.Attributes; |
| 216 | +using BenchmarkDotNet.Running; |
| 217 | + |
| 218 | +BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args); |
| 219 | + |
| 220 | +public class Bench |
| 221 | +{ |
| 222 | + [Benchmark] |
| 223 | + public void TestMethod() |
| 224 | + { |
| 225 | + // Benchmark code |
| 226 | + } |
| 227 | +} |
| 228 | +``` |
| 229 | +``` |
| 230 | +
|
| 231 | +### Example: Run Existing dotnet/performance Benchmarks |
| 232 | +
|
| 233 | +To run benchmarks from the dotnet/performance repository (no code snippet needed): |
| 234 | +
|
| 235 | +``` |
| 236 | +@EgorBot -arm -intel --filter `*TryGetValueFalse<String, String>*` |
| 237 | +``` |
| 238 | +
|
| 239 | +**Note**: Surround filter expressions with backticks to avoid issues with special characters. |
| 240 | +
|
| 241 | +## Important Notes |
| 242 | +
|
| 243 | +- **Bot response time**: EgorBot uses polling and may take up to 30 seconds to respond |
| 244 | +- **Supported repositories**: EgorBot monitors `dotnet/runtime` and `EgorBot/runtime-utils` |
| 245 | +- **PR mode (default)**: When posting in a PR, EgorBot automatically compares the PR changes against the base branch |
| 246 | +- **Results variability**: Results may vary between runs due to VM differences. Do not compare results across different architectures or cloud providers |
| 247 | +- **Check the manual**: EgorBot replies include a link to the [manual](https://github.com/EgorBot/runtime-utils) for advanced options |
| 248 | +
|
| 249 | +## Additional Resources |
| 250 | +
|
| 251 | +- [Microbenchmark Design Guidelines](https://github.com/dotnet/performance/blob/main/docs/microbenchmark-design-guidelines.md) - Essential reading for writing effective benchmarks |
| 252 | +- [BenchmarkDotNet CLI Arguments](https://github.com/dotnet/BenchmarkDotNet/blob/master/docs/articles/guides/console-args.md) |
| 253 | +- [EgorBot Manual](https://github.com/EgorBot/runtime-utils) |
| 254 | +- [BenchmarkDotNet Filter Simulator](http://egorbot.westus2.cloudapp.azure.com:5042/microbenchmarks) |
0 commit comments