BenchmarkDotNet measures the performance of focused .NET code. A benchmark resembles a unit test, but its most useful result is often a comparison between implementations, not an absolute running time. Wall-clock results can change with the environment. section 10.2
To compare two JSON deserializers:
- Create a C# console application and add BenchmarkDotNet, Newtonsoft.Json, and System.Text.Json. section 10.2
- Copy the JSON input to the build output and define the record it deserializes into. Both methods should use the same input. section 10.2
- Mark each method with [Benchmark] and run the class with BenchmarkRunner.Run<T>(). Return the deserialized value so the work is less likely to be discarded during Release compilation. section 10.2
Listing 10.4 Benchmark comparing JSON deserialization
using BenchmarkDotNet.Attributes; #1
using Newtonsoft.Json;
using STJ = System.Text.Json; #2
namespace FirstBenchmark;
public class JsonDeserializationTest
{
private readonly string _jsonString;
public JsonDeserializationTest()
{
var reader = new StreamReader("jokes.json"); #3
_jsonString = reader.ReadToEnd();
}
[Benchmark]
public Joke[]? Newtonsoft() =>
JsonConvert.DeserializeObject<Joke[]>(_jsonString); #4
[Benchmark]
public Joke[]? SystemText() =>
STJ.JsonSerializer.Deserialize<Joke[]>(_jsonString); #5
}
The two benchmark methods deserialize the same string into Joke arrays. Reading the file in the constructor keeps that setup outside the methods being compared. section 10.2
Check that benchmark methods work before timing them. One approach runs both methods and validates their results under DEBUG, then runs BenchmarkRunner under the other compilation path. In the sample result, System.Text.Json has a mean of 285.2 μs versus 657.7 μs for Newtonsoft.Json; those figures describe that run, not every environment. section 10.2
Listing 10.6 Using preprocessor directives
using BenchmarkDotNet.Running;
using FirstBenchmark;
#if DEBUG #1
var test = new JsonDeserializationTest();
CheckJokes(test.Newtonsoft); #2
CheckJokes(test.SystemText);
static void CheckJokes(Func<Joke[]?> func) #3
{
try
{
var jokes = func(); #4
if (jokes == null || !jokes.Any()) #5
{
throw new Exception();
}
Console.WriteLine("Succeeded");
}
catch
{
Console.WriteLine("Failed");
}
}
#else #6
BenchmarkRunner.Run<JsonDeserializationTest>();
#endif #7
The conditional entry point separates a quick correctness check from the benchmark run. section 10.2
BenchmarkDotNet can also capture profiles for individual benchmark methods. Event Tracing for Windows is the preferred Windows option and can include custom .NET events and disk access; Event Pipe works across platforms. Adding an EventPipeProfiler attribute produces .nettrace and .speedscope.json files in BenchmarkDotNet.Artifacts. section 10.3.1
Listing 10.7 Adding EventPipeProfiler to a benchmark class
[EventPipeProfiler(EventPipeProfile.CpuSampling)] #1
public class JsonDeserializationTest { }
Apply CPU-sampling Event Pipe profiling to a benchmark class with an attribute. section 10.3.1
Keep the measurement boundary narrow. BenchmarkDotNet suits code that runs in milliseconds or less and gives stable results. Network calls and database queries depend on external systems and are generally poor low-level benchmarks; use a higher-level load-testing tool such as NBomber for web API behavior. section 10.4
