diff --git a/.gitignore b/.gitignore index a90b6637143..732ca311a56 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ **/__pycache__/ *.pyc + +**/.vs/** +**/BenchmarkDotNet.Artifacts/** diff --git a/src/benchmarks/Benchmarks.csproj b/src/benchmarks/Benchmarks.csproj new file mode 100644 index 00000000000..44b4df6721f --- /dev/null +++ b/src/benchmarks/Benchmarks.csproj @@ -0,0 +1,33 @@ + + + + Exe + net46;netcoreapp2.0;netcoreapp2.1 + AnyCPU + pdbonly + true + true + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/benchmarks/Benchmarks.sln b/src/benchmarks/Benchmarks.sln new file mode 100644 index 00000000000..52db9697947 --- /dev/null +++ b/src/benchmarks/Benchmarks.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27428.2011 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks.csproj", "{D99F63AE-3154-4F13-9424-FA5F9D032D1D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D99F63AE-3154-4F13-9424-FA5F9D032D1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D99F63AE-3154-4F13-9424-FA5F9D032D1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D99F63AE-3154-4F13-9424-FA5F9D032D1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D99F63AE-3154-4F13-9424-FA5F9D032D1D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {59625A56-73BB-47B5-8B66-02F6918E41BD} + EndGlobalSection +EndGlobal diff --git a/src/benchmarks/NuGet.Config b/src/benchmarks/NuGet.Config new file mode 100644 index 00000000000..af68a668393 --- /dev/null +++ b/src/benchmarks/NuGet.Config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/benchmarks/Program.cs b/src/benchmarks/Program.cs new file mode 100644 index 00000000000..4ce997e15ac --- /dev/null +++ b/src/benchmarks/Program.cs @@ -0,0 +1,217 @@ +using System.Collections.Generic; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Exporters.Csv; +using BenchmarkDotNet.Horology; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.CoreRt; +using BenchmarkDotNet.Toolchains.CsProj; +using BenchmarkDotNet.Toolchains.CustomCoreClr; +using BenchmarkDotNet.Toolchains.DotNetCli; +using BenchmarkDotNet.Toolchains.InProcess; +using Benchmarks.Serializers; +using CommandLine; + +namespace Benchmarks +{ + class Program + { + static void Main(string[] args) + => Parser.Default.ParseArguments(args) + .WithParsed(RunBenchmarks) + .WithNotParsed(errors => { }); // ignore the errors, the parser prints nice error message + + private static void RunBenchmarks(Options options) + => BenchmarkSwitcher + .FromAssemblyAndTypes(typeof(Program).Assembly, SerializerBenchmarks.GetTypes()) + .Run(config: GetConfig(options)); + + private static IConfig GetConfig(Options options) + { + var baseJob = Job.ShortRun; // let's use the Short Run for better first user experience ;) + var jobs = GetJobs(options, baseJob).ToArray(); + + var config = DefaultConfig.Instance + .With(jobs.Any() ? jobs : new[] { baseJob }); + + if (options.UseMemoryDiagnoser) + config = config.With(MemoryDiagnoser.Default); + if (options.UseDisassemblyDiagnoser) + config = config.With(DisassemblyDiagnoser.Create(DisassemblyDiagnoserConfig.Asm)); + + if (options.DisplayAllStatistics) + config = config.With(StatisticColumn.AllStatistics); + + return config; + } + + private static IEnumerable GetJobs(Options options, Job baseJob) + { + if (options.RunInProcess) + yield return baseJob.With(InProcessToolchain.Instance); + + if (options.RunClr) + yield return baseJob.With(Runtime.Clr); + if (!string.IsNullOrEmpty(options.ClrVersion)) + yield return baseJob.With(new ClrRuntime(options.ClrVersion)); + + if (options.RunMono) + yield return baseJob.With(Runtime.Mono); + if (!string.IsNullOrEmpty(options.MonoPath)) + yield return baseJob.With(new MonoRuntime("Mono", options.MonoPath)); + + if (options.RunCoreRt) + yield return baseJob.With(Runtime.CoreRT).With(CoreRtToolchain.LatestMyGetBuild); + if (!string.IsNullOrEmpty(options.CoreRtVersion)) + yield return baseJob.With(Runtime.CoreRT) + .With(CoreRtToolchain.CreateBuilder() + .UseCoreRtNuGet(options.CoreRtVersion) + .AdditionalNuGetFeed("benchmarkdotnet ci", "https://ci.appveyor.com/nuget/benchmarkdotnet") + .ToToolchain()); + if (!string.IsNullOrEmpty(options.CoreRtPath)) + yield return baseJob.With(Runtime.CoreRT) + .With(CoreRtToolchain.CreateBuilder() + .UseCoreRtLocal(options.CoreRtPath) + .AdditionalNuGetFeed("benchmarkdotnet ci", "https://ci.appveyor.com/nuget/benchmarkdotnet") + .ToToolchain()); + + if (options.RunCore) + yield return baseJob.With(Runtime.Core).With(CsProjCoreToolchain.Current.Value); + if (options.RunCore20) + yield return baseJob.With(Runtime.Core).With(CsProjCoreToolchain.NetCoreApp20); + if (options.RunCore21) + yield return baseJob.With(Runtime.Core).With(CsProjCoreToolchain.NetCoreApp21); + + if (!string.IsNullOrEmpty(options.CoreFxVersion) || !string.IsNullOrEmpty(options.CoreClrVersion)) + { + var builder = CustomCoreClrToolchain.CreateBuilder(); + + if (!string.IsNullOrEmpty(options.CoreFxVersion) && !string.IsNullOrEmpty(options.CoreFxBinPackagesPath)) + builder.UseCoreFxLocalBuild(options.CoreFxVersion, options.CoreFxBinPackagesPath); + else if (!string.IsNullOrEmpty(options.CoreFxVersion)) + builder.UseCoreFxNuGet(options.CoreFxVersion); + else + builder.UseCoreFxDefault(); + + if (!string.IsNullOrEmpty(options.CoreClrVersion) && !string.IsNullOrEmpty(options.CoreClrBinPackagesPath) && !string.IsNullOrEmpty(options.CoreClrPackagesPath)) + builder.UseCoreClrLocalBuild(options.CoreClrVersion, options.CoreClrBinPackagesPath, options.CoreClrPackagesPath); + else if (!string.IsNullOrEmpty(options.CoreClrVersion)) + builder.UseCoreClrNuGet(options.CoreClrVersion); + else + builder.UseCoreClrDefault(); + + if (!string.IsNullOrEmpty(options.CliPath)) + builder.DotNetCli(options.CliPath); + + builder.AdditionalNuGetFeed("benchmarkdotnet ci", "https://ci.appveyor.com/nuget/benchmarkdotnet"); + + yield return baseJob.With(Runtime.Core).With(builder.ToToolchain()); + } + } + } + + public class Options + { + [Option("memory", Required = false, Default = true, HelpText = "Prints memory statistics. Enabled by default")] + public bool UseMemoryDiagnoser { get; set; } + + [Option("disassm", Required = false, Default = false, HelpText = "Gets diassembly for benchmarked code")] + public bool UseDisassemblyDiagnoser { get; set; } + + [Option("allStats", Required = false, Default = false, HelpText = "Displays all statistics (min, max & more")] + public bool DisplayAllStatistics { get; set; } + + [Option("inProcess", Required = false, Default = false, HelpText = "Run benchmarks in Process")] + public bool RunInProcess { get; set; } + + [Option("clr", Required = false, Default = false, HelpText = "Run benchmarks for Clr")] + public bool RunClr { get; set; } + + [Option("clrVersion", Required = false, HelpText = "Optional version of private CLR build used as the value of COMPLUS_Version env var.")] + public string ClrVersion { get; set; } + + [Option("mono", Required = false, Default = false, HelpText = "Run benchmarks for Mono (takes the default from PATH)")] + public bool RunMono { get; set; } + + [Option("monoPath", Required = false, HelpText = "Optional path to Mono which should be used for running benchmarks.")] + public string MonoPath { get; set; } + + [Option("coreRt", Required = false, Default = false, HelpText = "Run benchmarks for the latest CoreRT")] + public bool RunCoreRt { get; set; } + + [Option("coreRtVersion", Required = false, HelpText = "Optional version of Microsoft.DotNet.ILCompiler which should be used to run with CoreRT. Example: \"1.0.0-alpha-26414-01\"")] + public string CoreRtVersion { get; set; } + + [Option("ilcPath", Required = false, HelpText = "Optional IlcPath which should be used to run with private CoreRT build. Example: \"1.0.0-alpha-26414-01\"")] + public string CoreRtPath { get; set; } + + [Option("core", Required = false, Default = false, HelpText = "Run benchmarks for .NET Core")] + public bool RunCore { get; set; } + + [Option("core20", Required = false, Default = false, HelpText = "Run benchmarks for .NET Core 2.0")] + public bool RunCore20 { get; set; } + + [Option("core21", Required = false, Default = false, HelpText = "Run benchmarks for .NET Core 2.1")] + public bool RunCore21 { get; set; } + + [Option("cli", Required = false, HelpText = "Optional path to dotnet cli which should be used for running benchmarks.")] + public string CliPath { get; set; } + + [Option("coreClrVersion", Required = false, HelpText = "Optional version of Microsoft.NETCore.Runtime which should be used. Example: \"2.1.0-preview2-26305-0\"")] + public string CoreClrVersion { get; set; } + + [Option("coreClrBin", Required = false, HelpText = @"Optional path to folder with CoreClr NuGet packages. Example: ""C:\coreclr\bin\Product\Windows_NT.x64.Release\.nuget\pkg""")] + public string CoreClrBinPackagesPath { get; set; } + + [Option("coreClrPackages", Required = false, HelpText = @"Optional path to folder with NuGet packages restored for CoreClr build. Example: ""C:\Projects\coreclr\packages""")] + public string CoreClrPackagesPath { get; set; } + + [Option("coreFxVersion", Required = false, HelpText = "Optional version of Microsoft.Private.CoreFx.NETCoreApp which should be used. Example: \"4.5.0-preview2-26307-0\"")] + public string CoreFxVersion { get; set; } + + [Option("coreFxBin", Required = false, HelpText = @"Optional path to folder with CoreFX NuGet packages, Example: ""C:\Projects\forks\corefx\bin\packages\Release""")] + public string CoreFxBinPackagesPath { get; set; } + } + + /// + /// this config allows you to run benchmarks for multiple runtimes + /// + public class MultipleRuntimesConfig : ManualConfig + { + public MultipleRuntimesConfig() + { + Add(Job.Default.With(Runtime.Core).With(CsProjCoreToolchain.From(NetCoreAppSettings.NetCoreApp20)).AsBaseline().WithId("Core 2.0")); + Add(Job.Default.With(Runtime.Core).With(CsProjCoreToolchain.From(NetCoreAppSettings.NetCoreApp21)).WithId("Core 2.1")); + + Add(Job.Default.With(Runtime.Clr).WithId("Clr")); + Add(Job.Default.With(Runtime.Mono).WithId("Mono")); // you can comment this if you don't have Mono installed + Add(Job.Default.With(Runtime.CoreRT).WithId("CoreRT")); + + Add(MemoryDiagnoser.Default); + + Add(DefaultConfig.Instance.GetValidators().ToArray()); + Add(DefaultConfig.Instance.GetLoggers().ToArray()); + Add(DefaultConfig.Instance.GetColumnProviders().ToArray()); + + Add(new CsvMeasurementsExporter(CsvSeparator.Semicolon)); + //Add(RPlotExporter.Default); // it produces nice plots but requires R to be installed + Add(MarkdownExporter.GitHub); + Add(HtmlExporter.Default); + //Add(StatisticColumn.AllStatistics); + + Set(new BenchmarkDotNet.Reports.SummaryStyle + { + PrintUnitsInHeader = true, + PrintUnitsInContent = false, + TimeUnit = TimeUnit.Microsecond, + SizeUnit = SizeUnit.B + }); + } + } +} diff --git a/src/benchmarks/README.md b/src/benchmarks/README.md new file mode 100644 index 00000000000..2b560a3532f --- /dev/null +++ b/src/benchmarks/README.md @@ -0,0 +1,204 @@ +# Benchmarks + +This repo contains various .NET benchmarks. It uses BenchmarkDotNet as the benchmarking engine to run benchmarks for .NET, .NET Core, CoreRT and Mono. Including private runtime builds. + +## BenchmarkDotNet + +Benchmarking is really hard (especially microbenchmarking), you can easily make a mistake during performance measurements. +BenchmarkDotNet will protect you from the common pitfalls (even for experienced developers) because it does all the dirty work for you: + +* it generates an isolated project per runtime +* it builds the project in `Release` +* it runs every benchmark in a stand-alone process (to achieve process isolation and avoid side effects) +* it estimates the perfect invocation count per iteration +* it warms-up the code +* it evaluates the overhead +* it runs multiple iterations of the method until the requested level of precision is met. + +A few useful links for you: + +* If you want to know more about BenchmarkDotNet features, check out the [Overview Page](http://benchmarkdotnet.org/Overview.htm). +* If you want to use BenchmarkDotNet for the first time, the [Getting Started](http://benchmarkdotnet.org/GettingStarted.htm) will help you. +* If you want to ask a quick question or discuss performance topics, use the [gitter](https://gitter.im/dotnet/BenchmarkDotNet) channel. + +## Your first benchmark + +It's really easy to design a performance experiment with BenchmarkDotNet. Just mark your method with the `[Benchmark]` attribute and the benchmark is ready. + +```cs +public class Simple +{ + [Benchmark] + public byte[] CreateByteArray() => new byte[8]; +} +``` + +Any public, non-generic type with public `[Benchmark]` method in this assembly will be auto-detected and added to the benchmarks list. + +## Running + +To run the benchmarks you have to execute `dotnet run -c Release -f net46|netcoreapp2.0|netcoreapp2.1` (choose one of the supported frameworks). + +![Choose Benchmark](./img/chooseBenchmark.png) + +And select one of the benchmarks from the list by either entering it's number or name. To **run all** the benchmarks simply enter `*` to the console. + +BenchmarkDotNet will build the executables, run the benchmarks, print the results to console and **export the results** to `.\BenchmarkDotNet.Artifacts\results`. + +![Exported results](./img/exportedResults.png) + +BenchmarkDotNet by default exports the results to GitHub markdown, so you can just find the right `.md` file in `results` folder and copy-paste the markdown to GitHub. + +## All Statistics + +By default BenchmarkDotNet displays only `Mean`, `Error` and `StdDev` in the results. If you want to see more statistics, please pass `--allStats` as an extra argument to the app: `dotnet run -c Release -f netcoreapp2.1 -- --allStats`. If you build your own config, please use `config.With(StatisticColumn.AllStatistics)`. + +| Method | Mean | Error | StdDev | StdErr | Min | Q1 | Median | Q3 | Max | Op/s | Gen 0 | Allocated | +|--------- |---------:|----------:|----------:|----------:|---------:|---------:|---------:|---------:|---------:|------------:|-------:|----------:| +| Jil | 458.2 ns | 38.63 ns | 2.183 ns | 1.260 ns | 455.8 ns | 455.8 ns | 458.9 ns | 460.0 ns | 460.0 ns | 2,182,387.2 | 0.1163 | 736 B | +| JSON.NET | 869.8 ns | 47.37 ns | 2.677 ns | 1.545 ns | 867.7 ns | 867.7 ns | 868.8 ns | 872.8 ns | 872.8 ns | 1,149,736.0 | 0.2394 | 1512 B | +| Utf8Json | 272.6 ns | 341.64 ns | 19.303 ns | 11.145 ns | 256.7 ns | 256.7 ns | 266.9 ns | 294.1 ns | 294.1 ns | 3,668,854.8 | 0.0300 | 192 B | + +## How to read the Memory Statistics + +The project is configured to include managed memory statistics by using [Memory Diagnoser](http://adamsitnik.com/the-new-Memory-Diagnoser/) + +| Method | Gen 0 | Allocated | +|----------- |------- |---------- | +| A | - | 0 B | +| B | 1 | 496 B | + +* Allocated contains the size of allocated **managed** memory. **Stackalloc/native heap allocations are not included.** It's per single invocation, **inclusive**. +* The `Gen X` column contains the number of `Gen X` collections per ***1 000*** Operations. If the value is equal 1, then it means that GC collects memory once per one thousand of benchmark invocations in generation `X`. BenchmarkDotNet is using some heuristic when running benchmarks, so the number of invocations can be different for different runs. Scaling makes the results comparable. +* `-` in the Gen column means that no garbage collection was performed. +* If `Gen X` column is not present, then it means that no garbage collection was performed for generation `X`. If none of your benchmarks induces the GC, the Gen columns are not present. + +## How to get the Disassembly + +If you want to disassemble the benchmarked code, you need to use the [Disassembly Diagnoser](http://adamsitnik.com/Disassembly-Diagnoser/). It allows to disassemble `asm/C#/IL` in recursive way on Windows for .NET and .NET Core (all Jits) and `asm` for Mono on any OS. + +You can do that by passing `--disassm` to the app or by using `[DisassemblyDiagnoser(printAsm: true, printSource: true)]` attribute or by adding it to your config with `config.With(DisassemblyDiagnoser.Create(new DisassemblyDiagnoserConfig(printAsm: true, recursiveDepth: 1))`. + +![Sample Disassm](./img/sampleDisassm.png) + +## How to run In Process + +If you want to run the benchmarks in process, without creating a dedicated executable and process-level isolation, please pass `--inProcess` as an extra argument to the app: `dotnet run -c Release -f netcoreapp2.1 -- --inProcess`. If you build your own config, please use `config.With(Job.Default.With(InProcessToolchain.Instance))`. Please use this option only when you are sure that the benchmarks you want to run have no side effects. + +## How to compare different Runtimes + +BenchmarkDotNet allows you to run benchmarks for multiple runtimes. By using this feature you can compare .NET vs .NET Core vs CoreRT vs Mono or .NET Core 2.0 vs .NET Core 2.1. BDN will compile and run the right stuff for you. + +* for .NET pass `--clr` to the app or use `Job.Default.With(Runtime.Clr)` in the code. +* for .NET Core 2.0 pass `--core20` to the app or use `Job.Default.With(Runtime.Core).With(CsProjCoreToolchain.NetCoreApp20)` in the code. +* for .NET Core 2.1 pass `--core21` to the app or use `Job.Default.With(Runtime.Core).With(CsProjCoreToolchain.NetCoreApp20)` in the code. +* for the latest CoreRT pass `--coreRt` to the app or use `Job.Default.With(Runtime.CoreRT).With(CoreRtToolchain.LatestMyGetBuild)` in the code. **Be warned!** Downloading latest CoreRT with all the dependencies takes a lot of time. It is recommended to choose one version and use it for comparisions, more info [here](https://github.com/dotnet/BenchmarkDotNet/blob/600e5fa81bd8e7a1d32a60b2bea830e1f46106eb/docs/guide/Configs/Toolchains.md#corert). To use explicit CoreRT version please use `coreRtVersion` argument. Example: `dotnet run -c Release -f netcoreapp2.1 --coreRtVersion 1.0.0-alpha-26414-0` +* for Mono pass `--mono` to the app or use `Job.Default.With(Runtime.Mono)` in the code. + +An example command for comparing 4 runtimes: `dotnet run -c Release -f netcoreapp2.1 -- --core20 --core21 --mono --clr --coreRt` + +``` ini +BenchmarkDotNet=v0.10.14.516-nightly, OS=Windows 10.0.16299.309 (1709/FallCreatorsUpdate/Redstone3) +Intel Xeon CPU E5-1650 v4 3.60GHz, 1 CPU, 12 logical and 6 physical cores +Frequency=3507504 Hz, Resolution=285.1030 ns, Timer=TSC +.NET Core SDK=2.1.300-preview1-008174 + [Host] : .NET Core 2.1.0-preview1-26216-03 (CoreCLR 4.6.26216.04, CoreFX 4.6.26216.02), 64bit RyuJIT + Job-GALXOG : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2633.0 + Job-DRRTOZ : .NET Core 2.0.6 (CoreCLR 4.6.26212.01, CoreFX 4.6.26212.01), 64bit RyuJIT + Job-QQFGIW : .NET Core 2.1.0-preview1-26216-03 (CoreCLR 4.6.26216.04, CoreFX 4.6.26216.02), 64bit RyuJIT + Job-GKRDGF : .NET CoreRT 1.0.26412.02, 64bit AOT + Job-HNFRHF : Mono 5.10.0 (Visual Studio), 64bit + +LaunchCount=1 TargetCount=3 WarmupCount=3 +``` + +| Method | Runtime | Toolchain | Mean | Error | StdDev | Allocated | +|--------- |-------- |----------------------------- |----------:|-----------:|----------:|----------:| +| ParseInt | Clr | Default | 95.95 ns | 5.354 ns | 0.3025 ns | 0 B | +| ParseInt | Core | .NET Core 2.0 | 104.71 ns | 121.620 ns | 6.8718 ns | 0 B | +| ParseInt | Core | .NET Core 2.1 | 93.16 ns | 6.383 ns | 0.3606 ns | 0 B | +| ParseInt | CoreRT | Core RT 1.0.0-alpha-26412-02 | 110.02 ns | 71.947 ns | 4.0651 ns | 0 B | +| ParseInt | Mono | Default | 133.19 ns | 133.928 ns | 7.5672 ns | N/A | + +## .NET Core 2.0 vs .NET Core 2.1 + +If you want to compare .NET Core 2.0 vs .NET Core 2.1 you can just pass `-- --core20 --core21`. You can also build a custom config and mark selected runtime as baseline, then all the results will be scaled to the baseline. + +```cs +Add(Job.Default.With(Runtime.Core).With(CsProjCoreToolchain.From(NetCoreAppSettings.NetCoreApp20)).WithId("Core 2.0").AsBaseline()); +Add(Job.Default.With(Runtime.Core).With(CsProjCoreToolchain.From(NetCoreAppSettings.NetCoreApp21)).WithId("Core 2.1")); +``` + +| Method | Job | Toolchain | IsBaseline | Mean | Error | StdDev | Scaled | +|---------------------- |--------- |-------------- |----------- |---------:|----------:|----------:|-------:| +| CompactLoopBodyLayout | Core 2.0 | .NET Core 2.0 | True | 36.72 ns | 0.1583 ns | 0.1481 ns | 1.00 | +| CompactLoopBodyLayout | Core 2.1 | .NET Core 2.1 | Default | 30.47 ns | 0.1731 ns | 0.1619 ns | 0.83 | + +## Benchmarking private CLR build + +It's possible to benchmark a private build of .NET Runtime. You just need to pass the value of `COMPLUS_Version` to BenchmarkDotNet. You can do that by either using `--clrVersion $theVersion` as an arugment or `Job.ShortRun.With(new ClrRuntime(version: "$theVersiong"))` in the code. + +So if you made a change in CLR and want to measure the difference, you can run the benchmarks with `dotnet run -c Release -f net46 --clr --clrVersion $theVersion`. More info can be found [here](https://github.com/dotnet/BenchmarkDotNet/issues/706). + +## Any CoreCLR and CoreFX + +BenchmarkDotNet allows the users to run their benchmarks against ANY CoreCLR and CoreFX builds. You can compare your local build vs MyGet feed or Debug vs Release or one version vs another. + +To avoid problems described [here](https://github.com/dotnet/coreclr/blob/master/Documentation/workflow/UsingDotNetCli.md#update-coreclr-using-runtime-nuget-package) a temporary folder is used when restoring packages for local builds. This is why it takes 20-30s in total to build the benchmarks. + +Entire feature with many examples is described [here](https://github.com/dotnet/BenchmarkDotNet/blob/600e5fa81bd8e7a1d32a60b2bea830e1f46106eb/docs/guide/Configs/Toolchains.md#custom-coreclr-and-corefx). + +### Benchmarking private CoreFX build + +To run benchmarks with private CoreFX build you need to provide the version of `Microsoft.Private.CoreFx.NETCoreApp` and the path to folder with CoreFX NuGet packages. + +Sample arguments: `dotnet run -c Release -f netcoreapp2.1 -- --coreFxBin C:\Projects\forks\corefx\bin\packages\Release --coreFxVersion 4.5.0-preview2-26307-0` + +Sample config: + +```cs +Job.ShortRun.With( + CustomCoreClrToolchain.CreateBuilder() + .UseCoreFxLocalBuild("4.5.0-preview2-26313-0", @"C:\Projects\forks\corefx\bin\packages\Release") + .UseCoreClrDefault() + .AdditionalNuGetFeed("benchmarkdotnet ci", "https://ci.appveyor.com/nuget/benchmarkdotnet"); + .DisplayName("local corefx") + .ToToolchain()); +``` + +### Benchmarking private CoreCLR build + +To run benchmarks with private CoreCLR build you need to provide the version of `Microsoft.NETCore.Runtime`, path to folder with CoreCLR NuGet packages and path to `coreclr\packages` folder. + +Sample arguments: `dotnet run -c Release -f netcoreapp2.1 -- --coreClrBin C:\coreclr\bin\Product\Windows_NT.x64.Release\.nuget\pkg --coreClrPackages C:\Projects\coreclr\packages --coreClrVersion 2.1.0-preview2-26305-0` + +Sample config: + +```cs +Job.ShortRun.With( + CustomCoreClrToolchain.CreateBuilder() + .UseCoreClrLocalBuild("2.1.0-preview2-26313-0", @"C:\Projects\forks\coreclr\bin\Product\Windows_NT.x64.Release\.nuget\pkg", @"C:\Projects\coreclr\packages") + .UseCoreFxDefault() + .AdditionalNuGetFeed("benchmarkdotnet ci", "https://ci.appveyor.com/nuget/benchmarkdotnet"); + .DisplayName("local builds") + .ToToolchain()); +``` + +## Benchmarking private CoreRT build + +To run benchmarks with private CoreRT build you need to provide the `IlcPath`. + +Sample arguments: `dotnet run -c Release -f netcoreapp2.1 -- --ilcPath C:\Projects\corert\bin\Windows_NT.x64.Release` + +Sample config: + +```cs +var config = DefaultConfig.Instance + .With(Job.ShortRun + .With(Runtime.CoreRT) + .With(CoreRtToolchain.CreateBuilder() + .UseCoreRtLocal(@"C:\Projects\corert\bin\Windows_NT.x64.Release") // IlcPath + .DisplayName("Core RT RyuJit") + .ToToolchain())); +``` + diff --git a/src/benchmarks/Serializers/Binary_FromStream.cs b/src/benchmarks/Serializers/Binary_FromStream.cs new file mode 100644 index 00000000000..7e024eb7661 --- /dev/null +++ b/src/benchmarks/Serializers/Binary_FromStream.cs @@ -0,0 +1,97 @@ +using System; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; +using BenchmarkDotNet.Attributes; +using Benchmarks.Serializers.Helpers; + +namespace Benchmarks.Serializers +{ + public class Binary_FromStream where T : IVerifiable + { + private readonly T value; + private readonly MemoryStream memoryStream; + private readonly BinaryFormatter binaryFormatter; + + public Binary_FromStream() + { + value = DataGenerator.Generate(); + + // the stream is pre-allocated, we don't want the benchmarks to include stream allocaton cost + memoryStream = new MemoryStream(capacity: short.MaxValue); + binaryFormatter = new BinaryFormatter(); + + ProtoBuf.Meta.RuntimeTypeModel.Default.Add(typeof(DateTimeOffset), false).SetSurrogate(typeof(DateTimeOffsetSurrogate)); // https://stackoverflow.com/a/7046868 + } + + [IterationSetup(Target = nameof(BinaryFormatter_))] + public void SetupBinaryFormatter() + { + memoryStream.Position = 0; + binaryFormatter.Serialize(memoryStream, value); + } + + [IterationSetup(Target = nameof(ProtoBuffNet))] + public void SetupProtoBuffNet() + { + memoryStream.Position = 0; + ProtoBuf.Serializer.Serialize(memoryStream, value); + } + + [IterationSetup(Target = nameof(ZeroFormatter_Naive) + "," + nameof(ZeroFormatter_Real))] + public void SetupZeroFormatter_() + { + memoryStream.Position = 0; + ZeroFormatter.ZeroFormatterSerializer.Serialize(memoryStream, value); + } + + [IterationSetup(Target = nameof(MessagePack_))] + public void SetupMessagePack() + { + memoryStream.Position = 0; + MessagePack.MessagePackSerializer.Serialize(memoryStream, value); + } + + [Benchmark(Description = nameof(BinaryFormatter))] + public T BinaryFormatter_() + { + memoryStream.Position = 0; + return (T)binaryFormatter.Deserialize(memoryStream); + } + + [Benchmark(Description = "protobuf-net")] + public T ProtoBuffNet() + { + memoryStream.Position = 0; + return ProtoBuf.Serializer.Deserialize(memoryStream); + } + + [Benchmark(Description = "ZeroFormatter_Naive")] + public T ZeroFormatter_Naive() + { + memoryStream.Position = 0; + return ZeroFormatter.ZeroFormatterSerializer.Deserialize(memoryStream); + } + + /// + /// ZeroFormatter requires all properties to be virtual + /// they are deserialized for real when they are used for the first time + /// if we don't touch the properites, they are not being deserialized and the result is skewed + /// + [Benchmark(Description = "ZeroFormatter_Real")] + public long ZeroFormatter_Real() + { + memoryStream.Position = 0; + + var deserialized = ZeroFormatter.ZeroFormatterSerializer.Deserialize(memoryStream); + + return deserialized.TouchEveryProperty(); + } + + [Benchmark(Description = "MessagePack")] + public T MessagePack_() + { + memoryStream.Position = 0; + return MessagePack.MessagePackSerializer.Deserialize(memoryStream); + } + } +} diff --git a/src/benchmarks/Serializers/Binary_ToStream.cs b/src/benchmarks/Serializers/Binary_ToStream.cs new file mode 100644 index 00000000000..9ba63a593ea --- /dev/null +++ b/src/benchmarks/Serializers/Binary_ToStream.cs @@ -0,0 +1,54 @@ +using System; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; +using BenchmarkDotNet.Attributes; +using Benchmarks.Serializers.Helpers; + +namespace Benchmarks.Serializers +{ + public class Binary_ToStream + { + private readonly T value; + private readonly MemoryStream memoryStream; + private readonly BinaryFormatter binaryFormatter; + + public Binary_ToStream() + { + value = DataGenerator.Generate(); + + // the stream is pre-allocated, we don't want the benchmarks to include stream allocaton cost + memoryStream = new MemoryStream(capacity: short.MaxValue); + binaryFormatter = new BinaryFormatter(); + + ProtoBuf.Meta.RuntimeTypeModel.Default.Add(typeof(DateTimeOffset), false).SetSurrogate(typeof(DateTimeOffsetSurrogate)); // https://stackoverflow.com/a/7046868 + } + + [Benchmark(Description = nameof(BinaryFormatter))] + public void BinaryFormatter_() + { + memoryStream.Position = 0; + binaryFormatter.Serialize(memoryStream, value); + } + + [Benchmark(Description = "protobuf-net")] + public void ProtoBuffNet() + { + memoryStream.Position = 0; + ProtoBuf.Serializer.Serialize(memoryStream, value); + } + + [Benchmark(Description = "ZeroFormatter")] + public void ZeroFormatter_() + { + memoryStream.Position = 0; + ZeroFormatter.ZeroFormatterSerializer.Serialize(memoryStream, value); + } + + [Benchmark(Description = "MessagePack")] + public void MessagePack_() + { + memoryStream.Position = 0; + MessagePack.MessagePackSerializer.Serialize(memoryStream, value); + } + } +} diff --git a/src/benchmarks/Serializers/DataGenerator.cs b/src/benchmarks/Serializers/DataGenerator.cs new file mode 100644 index 00000000000..4b9c834856d --- /dev/null +++ b/src/benchmarks/Serializers/DataGenerator.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MessagePack; +using ProtoBuf; +using ZeroFormatter; + +namespace Benchmarks.Serializers +{ + internal static class DataGenerator + { + internal static T Generate() + { + if (typeof(T) == typeof(LoginViewModel)) + return (T)(object)CreateLoginViewModel(); + if (typeof(T) == typeof(Location)) + return (T)(object)CreateLocation(); + if (typeof(T) == typeof(IndexViewModel)) + return (T)(object)CreateIndexViewModel(); + if (typeof(T) == typeof(MyEventsListerViewModel)) + return (T)(object)CreateMyEventsListerViewModel(); + + throw new NotImplementedException(); + } + + private static LoginViewModel CreateLoginViewModel() + => new LoginViewModel + { + Email = "name.familyname@not.com", + Password = "abcdefgh123456!@", + RememberMe = true + }; + + private static Location CreateLocation() + => new Location + { + Id = 1234, + Address1 = "The Street Name", + Address2 = "20/11", + City = "The City", + State = "The State", + PostalCode = "abc-12", + Name = "Nonexisting", + PhoneNumber = "+0 11 222 333 44", + Country = "The Greatest" + }; + + private static IndexViewModel CreateIndexViewModel() + => new IndexViewModel + { + IsNewAccount = false, + FeaturedCampaign = new CampaignSummaryViewModel + { + Description = "Very nice campaing", + Headline = "The Headline", + Id = 234235, + OrganizationName = "The Company XYZ", + ImageUrl = "https://www.dotnetfoundation.org/theme/img/carousel/foundation-diagram-content.png", + Title = "Promoting Open Source" + }, + ActiveOrUpcomingEvents = Enumerable.Repeat( + new ActiveOrUpcomingEvent + { + Id = 10, + CampaignManagedOrganizerName = "Name FamiltyName", + CampaignName = "The very new campaing", + Description = "The .NET Foundation works with Microsoft and the broader industry to increase the exposure of open source projects in the .NET community and the .NET Foundation. The .NET Foundation provides access to these resources to projects and looks to promote the activities of our communities.", + EndDate = DateTime.UtcNow.AddYears(1), + Name = "Just a name", + ImageUrl = "https://www.dotnetfoundation.org/theme/img/carousel/foundation-diagram-content.png", + StartDate = DateTime.UtcNow + }, + count: 20).ToList() + }; + + private static MyEventsListerViewModel CreateMyEventsListerViewModel() + => new MyEventsListerViewModel + { + CurrentEvents = Enumerable.Repeat(CreateMyEventsListerItem(), 3).ToList(), + FutureEvents = Enumerable.Repeat(CreateMyEventsListerItem(), 9).ToList(), + PastEvents = Enumerable.Repeat(CreateMyEventsListerItem(), 60).ToList() // usually there is a lot of historical data + }; + + private static MyEventsListerItem CreateMyEventsListerItem() + => new MyEventsListerItem + { + Campaign = "A very nice campaing", + EndDate = DateTime.UtcNow.AddDays(7), + EventId = 321, + EventName = "wonderful name", + Organization = "Local Animal Shelter", + StartDate = DateTime.UtcNow.AddDays(-7), + TimeZone = TimeZoneInfo.Utc.DisplayName, + VolunteerCount = 15, + Tasks = Enumerable.Repeat( + new MyEventsListerItemTask + { + StartDate = DateTime.UtcNow, + EndDate = DateTime.UtcNow.AddDays(1), + Name = "A very nice task to have" + }, 4).ToList() + }; + } + + /// + /// ZeroFormatter requires all properties to be virtual + /// they are deserialized for real when they are used for the first time + /// if we don't touch the properites, they are not being deserialized and the result is skewed + /// + public interface IVerifiable + { + long TouchEveryProperty(); + } + + // the view models come from a real world app called "AllReady" + [Serializable] + [ProtoContract] + [ZeroFormattable] + [MessagePackObject] + public class LoginViewModel : IVerifiable + { + [ProtoMember(1)] [Index(0)] [Key(0)] public virtual string Email { get; set; } + [ProtoMember(2)] [Index(1)] [Key(1)] public virtual string Password { get; set; } + [ProtoMember(3)] [Index(2)] [Key(2)] public virtual bool RememberMe { get; set; } + + public long TouchEveryProperty() => Email.Length + Password.Length + Convert.ToInt32(RememberMe); + } + + [Serializable] + [ProtoContract] + [ZeroFormattable] + [MessagePackObject] + public class Location : IVerifiable + { + [ProtoMember(1)] [Index(0)] [Key(0)] public virtual int Id { get; set; } + [ProtoMember(2)] [Index(1)] [Key(1)] public virtual string Address1 { get; set; } + [ProtoMember(3)] [Index(2)] [Key(2)] public virtual string Address2 { get; set; } + [ProtoMember(4)] [Index(3)] [Key(3)] public virtual string City { get; set; } + [ProtoMember(5)] [Index(4)] [Key(4)] public virtual string State { get; set; } + [ProtoMember(6)] [Index(5)] [Key(5)] public virtual string PostalCode { get; set; } + [ProtoMember(7)] [Index(6)] [Key(6)] public virtual string Name { get; set; } + [ProtoMember(8)] [Index(7)] [Key(7)] public virtual string PhoneNumber { get; set; } + [ProtoMember(9)] [Index(8)] [Key(8)] public virtual string Country { get; set; } + + public long TouchEveryProperty() => Id + Address1.Length + Address2.Length + City.Length + State.Length + PostalCode.Length + Name.Length + PhoneNumber.Length + Country.Length; + } + + [Serializable] + [ProtoContract] + [ZeroFormattable] + [MessagePackObject] + public class ActiveOrUpcomingCampaign : IVerifiable + { + [ProtoMember(1)] [Index(0)] [Key(0)] public virtual int Id { get; set; } + [ProtoMember(2)] [Index(1)] [Key(1)] public virtual string ImageUrl { get; set; } + [ProtoMember(3)] [Index(2)] [Key(2)] public virtual string Name { get; set; } + [ProtoMember(4)] [Index(3)] [Key(3)] public virtual string Description { get; set; } + [ProtoMember(5)] [Index(4)] [Key(4)] public virtual DateTimeOffset StartDate { get; set; } + [ProtoMember(6)] [Index(5)] [Key(5)] public virtual DateTimeOffset EndDate { get; set; } + + public long TouchEveryProperty() => Id + ImageUrl.Length + Name.Length + Description.Length + StartDate.Ticks + EndDate.Ticks; + } + + [Serializable] + [ProtoContract] + [ZeroFormattable] + [MessagePackObject] + public class ActiveOrUpcomingEvent : IVerifiable + { + [ProtoMember(1)] [Index(0)] [Key(0)] public virtual int Id { get; set; } + [ProtoMember(2)] [Index(1)] [Key(1)] public virtual string ImageUrl { get; set; } + [ProtoMember(3)] [Index(2)] [Key(2)] public virtual string Name { get; set; } + [ProtoMember(4)] [Index(3)] [Key(3)] public virtual string CampaignName { get; set; } + [ProtoMember(5)] [Index(4)] [Key(4)] public virtual string CampaignManagedOrganizerName { get; set; } + [ProtoMember(6)] [Index(5)] [Key(5)] public virtual string Description { get; set; } + [ProtoMember(7)] [Index(6)] [Key(6)] public virtual DateTimeOffset StartDate { get; set; } + [ProtoMember(8)] [Index(7)] [Key(7)] public virtual DateTimeOffset EndDate { get; set; } + + public long TouchEveryProperty() => Id + ImageUrl.Length + Name.Length + CampaignName.Length + CampaignManagedOrganizerName.Length + Description.Length + StartDate.Ticks + EndDate.Ticks; + } + + [Serializable] + [ProtoContract] + [ZeroFormattable] + [MessagePackObject] + public class CampaignSummaryViewModel : IVerifiable + { + [ProtoMember(1)] [Index(0)] [Key(0)] public virtual int Id { get; set; } + [ProtoMember(2)] [Index(1)] [Key(1)] public virtual string Title { get; set; } + [ProtoMember(3)] [Index(2)] [Key(2)] public virtual string Description { get; set; } + [ProtoMember(4)] [Index(3)] [Key(3)] public virtual string ImageUrl { get; set; } + [ProtoMember(5)] [Index(4)] [Key(4)] public virtual string OrganizationName { get; set; } + [ProtoMember(6)] [Index(5)] [Key(5)] public virtual string Headline { get; set; } + + public long TouchEveryProperty() => Id + Title.Length + Description.Length + ImageUrl.Length + OrganizationName.Length + Headline.Length; + } + + [Serializable] + [ProtoContract] + [ZeroFormattable] + [MessagePackObject] + public class IndexViewModel : IVerifiable + { + [ProtoMember(1)] [Index(0)] [Key(0)] public virtual List ActiveOrUpcomingEvents { get; set; } + [ProtoMember(2)] [Index(1)] [Key(1)] public virtual CampaignSummaryViewModel FeaturedCampaign { get; set; } + [ProtoMember(3)] [Index(2)] [Key(2)] public virtual bool IsNewAccount { get; set; } + [IgnoreFormat] [IgnoreMember] public bool HasFeaturedCampaign => FeaturedCampaign != null; + + public long TouchEveryProperty() + { + long result = FeaturedCampaign.TouchEveryProperty() + Convert.ToInt32(IsNewAccount); + + for (int i = 0; i < ActiveOrUpcomingEvents.Count; i++) // no LINQ here to prevent from skewing allocations results + result += ActiveOrUpcomingEvents[i].TouchEveryProperty(); + + return result; + } + } + + [Serializable] + [ProtoContract] + [ZeroFormattable] + [MessagePackObject] + public class MyEventsListerViewModel : IVerifiable + { + // the orginal type defined these fields as IEnumerable, + // but XmlSerializer failed to serialize them with "cannot serialize member because it is an interface" error + [ProtoMember(1)] [Index(0)] [Key(0)] public virtual List CurrentEvents { get; set; } = new List(); + [ProtoMember(2)] [Index(1)] [Key(1)] public virtual List FutureEvents { get; set; } = new List(); + [ProtoMember(3)] [Index(2)] [Key(2)] public virtual List PastEvents { get; set; } = new List(); + + public long TouchEveryProperty() + { + long result = 0; + + // no LINQ here to prevent from skewing allocations results + for (int i = 0; i < CurrentEvents.Count; i++) result += CurrentEvents[i].TouchEveryProperty(); + for (int i = 0; i < FutureEvents.Count; i++) result += FutureEvents[i].TouchEveryProperty(); + for (int i = 0; i < PastEvents.Count; i++) result += PastEvents[i].TouchEveryProperty(); + + return result; + } + } + + [Serializable] + [ProtoContract] + [ZeroFormattable] + [MessagePackObject] + public class MyEventsListerItem : IVerifiable + { + [ProtoMember(1)] [Index(0)] [Key(0)] public virtual int EventId { get; set; } + [ProtoMember(2)] [Index(1)] [Key(1)] public virtual string EventName { get; set; } + [ProtoMember(3)] [Index(2)] [Key(2)] public virtual DateTimeOffset StartDate { get; set; } + [ProtoMember(4)] [Index(3)] [Key(3)] public virtual DateTimeOffset EndDate { get; set; } + [ProtoMember(5)] [Index(4)] [Key(4)] public virtual string TimeZone { get; set; } + [ProtoMember(6)] [Index(5)] [Key(5)] public virtual string Campaign { get; set; } + [ProtoMember(7)] [Index(6)] [Key(6)] public virtual string Organization { get; set; } + [ProtoMember(8)] [Index(7)] [Key(7)] public virtual int VolunteerCount { get; set; } + + [ProtoMember(9)] [Index(8)] [Key(8)] public virtual List Tasks { get; set; } = new List(); + + public long TouchEveryProperty() + { + long result = EventId + EventName.Length + StartDate.Ticks + EndDate.Ticks + TimeZone.Length + Campaign.Length + Organization.Length + VolunteerCount; + + for (int i = 0; i < Tasks.Count; i++) // no LINQ here to prevent from skewing allocations results + result += Tasks[i].TouchEveryProperty(); + + return result; + } + } + + [Serializable] + [ProtoContract] + [ZeroFormattable] + [MessagePackObject] + public class MyEventsListerItemTask : IVerifiable + { + [ProtoMember(1)] [Index(0)] [Key(0)] public virtual string Name { get; set; } + [ProtoMember(2)] [Index(1)] [Key(1)] public virtual DateTimeOffset? StartDate { get; set; } + [ProtoMember(3)] [Index(2)] [Key(2)] public virtual DateTimeOffset? EndDate { get; set; } + + [IgnoreFormat] + [IgnoreMember] + public string FormattedDate + { + get + { + if (!StartDate.HasValue || !EndDate.HasValue) + { + return null; + } + + var startDateString = string.Format("{0:g}", StartDate.Value); + var endDateString = string.Format("{0:g}", EndDate.Value); + + return string.Format($"From {startDateString} to {endDateString}"); + } + } + + public long TouchEveryProperty() => Name.Length + StartDate.Value.Ticks + EndDate.Value.Ticks; + } +} \ No newline at end of file diff --git a/src/benchmarks/Serializers/Helpers/DateTimeOffsetSurrogate.cs b/src/benchmarks/Serializers/Helpers/DateTimeOffsetSurrogate.cs new file mode 100644 index 00000000000..fbb1e4196fe --- /dev/null +++ b/src/benchmarks/Serializers/Helpers/DateTimeOffsetSurrogate.cs @@ -0,0 +1,22 @@ +using System; +using ProtoBuf; + +namespace Benchmarks.Serializers.Helpers +{ + [ProtoContract] + public class DateTimeOffsetSurrogate + { + [ProtoMember(1)] + public string DateTimeString { get; set; } + + public static implicit operator DateTimeOffsetSurrogate(DateTimeOffset value) + { + return new DateTimeOffsetSurrogate { DateTimeString = value.ToString("u") }; + } + + public static implicit operator DateTimeOffset(DateTimeOffsetSurrogate value) + { + return DateTimeOffset.Parse(value.DateTimeString); + } + } +} diff --git a/src/benchmarks/Serializers/Json_FromStream.cs b/src/benchmarks/Serializers/Json_FromStream.cs new file mode 100644 index 00000000000..80e2555e7a0 --- /dev/null +++ b/src/benchmarks/Serializers/Json_FromStream.cs @@ -0,0 +1,109 @@ +using System.IO; +using System.Runtime.Serialization.Json; +using System.Text; +using BenchmarkDotNet.Attributes; + +namespace Benchmarks.Serializers +{ + public class Json_FromStream + { + private readonly T value; + + private readonly MemoryStream memoryStream; + + private DataContractJsonSerializer dataContractJsonSerializer; + private Newtonsoft.Json.JsonSerializer newtonSoftJsonSerializer; + + public Json_FromStream() + { + value = DataGenerator.Generate(); + + // the stream is pre-allocated, we don't want the benchmarks to include stream allocaton cost + memoryStream = new MemoryStream(capacity: short.MaxValue); + + dataContractJsonSerializer = new DataContractJsonSerializer(typeof(T)); + newtonSoftJsonSerializer = new Newtonsoft.Json.JsonSerializer(); + } + + [IterationSetup(Target = nameof(Jil_))] + public void SetupJil_() + { + memoryStream.Position = 0; + + using (var writer = new StreamWriter(memoryStream, Encoding.UTF8, short.MaxValue, leaveOpen: true)) + { + Jil.JSON.Serialize(value, writer); + writer.Flush(); + } + } + + [IterationSetup(Target = nameof(JsonNet_))] + public void SetupJsonNet_() + { + memoryStream.Position = 0; + + using (var writer = new StreamWriter(memoryStream, Encoding.UTF8, short.MaxValue, leaveOpen: true)) + { + newtonSoftJsonSerializer.Serialize(writer, value); + writer.Flush(); + } + } + + [IterationSetup(Target = nameof(Utf8Json_))] + public void SetupUtf8Json_() + { + memoryStream.Position = 0; + Utf8Json.JsonSerializer.Serialize(memoryStream, value); + } + + [IterationSetup(Target = nameof(DataContractJsonSerializer_))] + public void SetupDataContractJsonSerializer_() + { + memoryStream.Position = 0; + dataContractJsonSerializer.WriteObject(memoryStream, value); + } + + [Benchmark(Description = "Jil")] + public T Jil_() + { + memoryStream.Position = 0; + + using (var reader = CreateNonClosingReaderWithDefaultSizes()) + return Jil.JSON.Deserialize(reader); + } + + [Benchmark(Description = "JSON.NET")] + public T JsonNet_() + { + memoryStream.Position = 0; + + using (var reader = CreateNonClosingReaderWithDefaultSizes()) + return (T)newtonSoftJsonSerializer.Deserialize(reader, typeof(T)); + } + + [Benchmark(Description = "Utf8Json")] + public T Utf8Json_() + { + memoryStream.Position = 0; + return Utf8Json.JsonSerializer.Deserialize(memoryStream); + } + + [Benchmark(Description = "DataContractJsonSerializer")] + public T DataContractJsonSerializer_() + { + memoryStream.Position = 0; + return (T)dataContractJsonSerializer.ReadObject(memoryStream); + } + + private StreamReader CreateNonClosingReaderWithDefaultSizes() + => new StreamReader( + memoryStream, + Encoding.UTF8, + true, // default is true https://github.com/dotnet/corefx/blob/708e4537d8944199af7d580def0d97a030be98c7/src/Common/src/CoreLib/System/IO/StreamReader.cs#L98 + 1024, // default buffer size from CoreFX https://github.com/dotnet/corefx/blob/708e4537d8944199af7d580def0d97a030be98c7/src/Common/src/CoreLib/System/IO/StreamReader.cs#L27 + leaveOpen: true); // we want to reuse the same string in the benchmarks to make sure that cost of allocating stream is not included in the benchmarks + + [GlobalCleanup] + public void Cleanup() => memoryStream.Dispose(); + } +} diff --git a/src/benchmarks/Serializers/Json_FromString.cs b/src/benchmarks/Serializers/Json_FromString.cs new file mode 100644 index 00000000000..60866b839b8 --- /dev/null +++ b/src/benchmarks/Serializers/Json_FromString.cs @@ -0,0 +1,30 @@ +using BenchmarkDotNet.Attributes; + +namespace Benchmarks.Serializers +{ + public class Json_FromString + { + private readonly T value; + private string serialized; + + public Json_FromString() => value = DataGenerator.Generate(); + + [IterationSetup(Target = nameof(Jil_))] + public void SerializeJil() => serialized = Jil.JSON.Serialize(value); + + [IterationSetup(Target = nameof(JsonNet_))] + public void SerializeJsonNet() => serialized = Newtonsoft.Json.JsonConvert.SerializeObject(value); + + [IterationSetup(Target = nameof(Utf8Json_))] + public void SerializeUtf8Json_() => serialized = Utf8Json.JsonSerializer.ToJsonString(value); + + [Benchmark(Description = "Jil")] + public T Jil_() => Jil.JSON.Deserialize(serialized); + + [Benchmark(Description = "JSON.NET")] + public T JsonNet_() => Newtonsoft.Json.JsonConvert.DeserializeObject(serialized); + + [Benchmark(Description = "Utf8Json")] + public T Utf8Json_() => Utf8Json.JsonSerializer.Deserialize(serialized); + } +} diff --git a/src/benchmarks/Serializers/Json_ToStream.cs b/src/benchmarks/Serializers/Json_ToStream.cs new file mode 100644 index 00000000000..05113694f98 --- /dev/null +++ b/src/benchmarks/Serializers/Json_ToStream.cs @@ -0,0 +1,65 @@ +using System.IO; +using System.Runtime.Serialization.Json; +using System.Text; +using BenchmarkDotNet.Attributes; + +namespace Benchmarks.Serializers +{ + public class Json_ToStream + { + private readonly T value; + + private readonly MemoryStream memoryStream; + private readonly StreamWriter streamWriter; + + private DataContractJsonSerializer dataContractJsonSerializer; + private Newtonsoft.Json.JsonSerializer newtonSoftJsonSerializer; + + public Json_ToStream() + { + value = DataGenerator.Generate(); + + // the stream is pre-allocated, we don't want the benchmarks to include stream allocaton cost + memoryStream = new MemoryStream(capacity: short.MaxValue); + streamWriter = new StreamWriter(memoryStream, Encoding.UTF8); + + dataContractJsonSerializer = new DataContractJsonSerializer(typeof(T)); + newtonSoftJsonSerializer = new Newtonsoft.Json.JsonSerializer(); + } + + [Benchmark(Description = "Jil")] + public void Jil_() + { + memoryStream.Position = 0; + Jil.JSON.Serialize(value, streamWriter); + } + + [Benchmark(Description = "JSON.NET")] + public void JsonNet_() + { + memoryStream.Position = 0; + newtonSoftJsonSerializer.Serialize(streamWriter, value); + } + + [Benchmark(Description = "Utf8Json")] + public void Utf8Json_() + { + memoryStream.Position = 0; + Utf8Json.JsonSerializer.Serialize(memoryStream, value); + } + + [Benchmark(Description = "DataContractJsonSerializer")] + public void DataContractJsonSerializer_() + { + memoryStream.Position = 0; + dataContractJsonSerializer.WriteObject(memoryStream, value); + } + + [GlobalCleanup] + public void Cleanup() + { + streamWriter.Dispose(); + memoryStream.Dispose(); + } + } +} diff --git a/src/benchmarks/Serializers/Json_ToString.cs b/src/benchmarks/Serializers/Json_ToString.cs new file mode 100644 index 00000000000..8e64b197d2a --- /dev/null +++ b/src/benchmarks/Serializers/Json_ToString.cs @@ -0,0 +1,23 @@ +using BenchmarkDotNet.Attributes; + +namespace Benchmarks.Serializers +{ + public class Json_ToString + { + private readonly T value; + + public Json_ToString() => value = DataGenerator.Generate(); + + [Benchmark(Description = "Jil")] + public string Jil_() => Jil.JSON.Serialize(value); + + [Benchmark(Description = "JSON.NET")] + public string JsonNet_() => Newtonsoft.Json.JsonConvert.SerializeObject(value); + + [Benchmark(Description = "Utf8Json")] + public string Utf8Json_() => Utf8Json.JsonSerializer.ToJsonString(value); + + // DataContractJsonSerializer does not provide an API to serialize to string + // so it's not included here (apples vs apples thing) + } +} diff --git a/src/benchmarks/Serializers/README.md b/src/benchmarks/Serializers/README.md new file mode 100644 index 00000000000..86b425f47ef --- /dev/null +++ b/src/benchmarks/Serializers/README.md @@ -0,0 +1,34 @@ +# Serializers Benchmarks + +This folder contains benchmarks of the most popular serializers. + +## Serializers used (latest stable versions) + +* XML + * [XmlSerializer](https://docs.microsoft.com/en-us/dotnet/api/system.xml.serialization.xmlserializer) `4.3.0` +* JSON + * [DataContractJsonSerializer](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.json.datacontractjsonserializer) `4.3.0` + * [Jil](https://github.com/kevin-montrose/Jil) `2.15.4` + * [JSON.NET](https://github.com/JamesNK/Newtonsoft.Json) `11.0.1` + * [Utf8Json](https://github.com/neuecc/Utf8Json) `1.3.7` +* Binary + * [BinaryFormatter](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.formatters.binary.binaryformatter) `4.3.0` + * [MessagePack](https://github.com/neuecc/MessagePack-CSharp) `1.7.3.4` + * [protobuff-net](https://github.com/mgravell/protobuf-net) `2.3.7` + * [ZeroFormatter](https://github.com/neuecc/ZeroFormatter) `1.6.4` + +Missing: ProtoBuff from Google and BOND from MS + +## Data Contracts + +Data Contracts were copied from a real Web App – [allReady](https://github.com/HTBox/allReady/) to mimic real world scenarios. + +* [LoginViewModel](DataGenerator.cs#L120) – class, 3 properties +* [Location](DataGenerator.cs#L133) – class, 9 properties +* [IndexViewModel](DataGenerator.cs#L202) – class, nested class + list of 20 Events (8 properties each) +* [MyEventsListerViewModel](DataGenerator.cs#L224) - class, 3 lists of complex types, each type contains another list of complex types + +## Design Decisions + +1. We want to compare "apples to apples", so the benchmarks are divided into few groups: `ToStream`, `FromStream`, `ToString`, `FromString`. +2. Stream benchmarks write to pre-allocated MemoryStream, so the allocated bytes columns include only the cost of serialization. diff --git a/src/benchmarks/Serializers/SerializerBenchmarks.cs b/src/benchmarks/Serializers/SerializerBenchmarks.cs new file mode 100644 index 00000000000..225add3e09b --- /dev/null +++ b/src/benchmarks/Serializers/SerializerBenchmarks.cs @@ -0,0 +1,35 @@ +using System; +using System.Linq; + +namespace Benchmarks.Serializers +{ + internal static class SerializerBenchmarks + { + internal static Type[] GetTypes() + => GetOpenGenericBenchmarks() + .SelectMany(openGeneric => GetViewModels().Select(viewModel => openGeneric.MakeGenericType(viewModel))) + .ToArray(); + + private static Type[] GetOpenGenericBenchmarks() + => new[] + { + typeof(Json_ToString<>), + typeof(Json_ToStream<>), + typeof(Json_FromString<>), + typeof(Json_FromStream<>), + typeof(Xml_ToStream<>), + typeof(Xml_FromStream<>), + typeof(Binary_ToStream<>), + typeof(Binary_FromStream<>) + }; + + private static Type[] GetViewModels() + => new[] + { + typeof(LoginViewModel), + typeof(Location), + typeof(IndexViewModel), + typeof(MyEventsListerViewModel) + }; + } +} \ No newline at end of file diff --git a/src/benchmarks/Serializers/Xml_FromStream.cs b/src/benchmarks/Serializers/Xml_FromStream.cs new file mode 100644 index 00000000000..2060eeff53c --- /dev/null +++ b/src/benchmarks/Serializers/Xml_FromStream.cs @@ -0,0 +1,56 @@ +using System.IO; +using System.Runtime.Serialization; +using System.Xml.Serialization; +using BenchmarkDotNet.Attributes; + +namespace Benchmarks.Serializers +{ + public class Xml_FromStream + { + private readonly T value; + private readonly XmlSerializer xmlSerializer; + private readonly DataContractSerializer dataContractSerializer; + private readonly MemoryStream memoryStream; + + public Xml_FromStream() + { + value = DataGenerator.Generate(); + xmlSerializer = new XmlSerializer(typeof(T)); + dataContractSerializer = new DataContractSerializer(typeof(T)); + memoryStream = new MemoryStream(capacity: short.MaxValue); + } + + [IterationSetup(Target = nameof(XmlSerializer_))] + public void SetupXmlSerializer() + { + memoryStream.Position = 0; + xmlSerializer.Serialize(memoryStream, value); + } + + [IterationSetup(Target = nameof(DataContractSerializer_))] + public void SetupDataContractSerializer() + { + memoryStream.Position = 0; + dataContractSerializer.WriteObject(memoryStream, value); + } + + [Benchmark(Description = nameof(XmlSerializer))] + public T XmlSerializer_() + { + memoryStream.Position = 0; + return (T)xmlSerializer.Deserialize(memoryStream); + } + + [Benchmark(Description = nameof(DataContractSerializer))] + public T DataContractSerializer_() + { + memoryStream.Position = 0; + return (T)dataContractSerializer.ReadObject(memoryStream); + } + + // YAXSerializer is not included in the benchmarks because it does not allow to deserialize from stream (only from file and string) + + [GlobalCleanup] + public void Cleanup() => memoryStream.Dispose(); + } +} diff --git a/src/benchmarks/Serializers/Xml_ToStream.cs b/src/benchmarks/Serializers/Xml_ToStream.cs new file mode 100644 index 00000000000..2aa80876a30 --- /dev/null +++ b/src/benchmarks/Serializers/Xml_ToStream.cs @@ -0,0 +1,42 @@ +using System.IO; +using System.Runtime.Serialization; +using System.Xml.Serialization; +using BenchmarkDotNet.Attributes; + +namespace Benchmarks.Serializers +{ + public class Xml_ToStream + { + private readonly T value; + private readonly XmlSerializer xmlSerializer; + private readonly DataContractSerializer dataContractSerializer; + private readonly MemoryStream memoryStream; + + public Xml_ToStream() + { + value = DataGenerator.Generate(); + xmlSerializer = new XmlSerializer(typeof(T)); + dataContractSerializer = new DataContractSerializer(typeof(T)); + memoryStream = new MemoryStream(capacity: short.MaxValue); + } + + [Benchmark(Description = nameof(XmlSerializer))] + public void XmlSerializer_() + { + memoryStream.Position = 0; + xmlSerializer.Serialize(memoryStream, value); + } + + [Benchmark(Description = nameof(DataContractSerializer))] + public void DataContractSerializer_() + { + memoryStream.Position = 0; + dataContractSerializer.WriteObject(memoryStream, value); + } + + // YAXSerializer is not included in the benchmarks because it does not allow to serialize to stream (only to file and string) + + [GlobalCleanup] + public void Dispose() => memoryStream.Dispose(); + } +} diff --git a/src/benchmarks/img/chooseBenchmark.png b/src/benchmarks/img/chooseBenchmark.png new file mode 100644 index 00000000000..1114d8c7d4f Binary files /dev/null and b/src/benchmarks/img/chooseBenchmark.png differ diff --git a/src/benchmarks/img/exportedResults.png b/src/benchmarks/img/exportedResults.png new file mode 100644 index 00000000000..f4ddd8c5394 Binary files /dev/null and b/src/benchmarks/img/exportedResults.png differ diff --git a/src/benchmarks/img/sampleDisassm.png b/src/benchmarks/img/sampleDisassm.png new file mode 100644 index 00000000000..49ea7ea5090 Binary files /dev/null and b/src/benchmarks/img/sampleDisassm.png differ