From f55f176088193de995ebb59f92420ae419d0dd36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bry=C5=82ka?= Date: Mon, 23 Sep 2024 22:27:00 +0200 Subject: [PATCH] Add benchmark demo --- Benchmarks/BestPractices/BoundsCheck.cs | 83 ++++++++++++ Benchmarks/BestPractices/Boxing.cs | 41 ++++++ Benchmarks/BestPractices/BoxingUnsafe.cs | 38 ++++++ Benchmarks/BestPractices/CodeGen.cs | 43 +++++++ Benchmarks/BestPractices/Demo.md | 31 +++++ Benchmarks/BestPractices/GeneratorBench.cs | 142 +++++++++++++++++++++ Benchmarks/BestPractices/Nullable.cs | 34 +++++ Benchmarks/Program.cs | 2 +- 8 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 Benchmarks/BestPractices/BoundsCheck.cs create mode 100644 Benchmarks/BestPractices/Boxing.cs create mode 100644 Benchmarks/BestPractices/BoxingUnsafe.cs create mode 100644 Benchmarks/BestPractices/CodeGen.cs create mode 100644 Benchmarks/BestPractices/Demo.md create mode 100644 Benchmarks/BestPractices/GeneratorBench.cs create mode 100644 Benchmarks/BestPractices/Nullable.cs diff --git a/Benchmarks/BestPractices/BoundsCheck.cs b/Benchmarks/BestPractices/BoundsCheck.cs new file mode 100644 index 0000000..29eae40 --- /dev/null +++ b/Benchmarks/BestPractices/BoundsCheck.cs @@ -0,0 +1,83 @@ +using System.Runtime.InteropServices; + +namespace Benchmarks.BestPractices; + +/* +| Method | Runtime | Mean | Error | StdDev | Ratio | Allocated | +|------------ |--------- |---------:|--------:|--------:|---------:|----------:|- +| Get | .NET 6.0 | 708.6 ns | 2.23 ns | 1.74 ns | baseline | - | +| UnsafeGet | .NET 6.0 | 940.6 ns | 3.42 ns | 3.20 ns | +33% | - | +| SpanGet | .NET 6.0 | 432.0 ns | 5.55 ns | 5.19 ns | -39% | - | +| SpanListGet | .NET 6.0 | 426.9 ns | 1.05 ns | 0.93 ns | -40% | - | +| GetReverse | .NET 6.0 | 709.4 ns | 3.26 ns | 2.72 ns | +0% | - | +| | | | | | | | +| Get | .NET 8.0 | 477.2 ns | 7.30 ns | 6.47 ns | baseline | - | +| UnsafeGet | .NET 8.0 | 427.1 ns | 0.80 ns | 0.62 ns | -11% | - | +| SpanGet | .NET 8.0 | 297.4 ns | 2.26 ns | 1.89 ns | -38% | - | +| SpanListGet | .NET 8.0 | 299.0 ns | 3.41 ns | 3.02 ns | -37% | - | +| GetReverse | .NET 8.0 | 355.5 ns | 1.50 ns | 1.25 ns | -26% | - | + */ +public class BoundsCheck +{ + private const int SIZE = 1000; + private static readonly int[] _array = Enumerable.Range(0, SIZE).ToArray(); + private static readonly List _list = Enumerable.Range(0, SIZE).ToList(); + + [Benchmark(Baseline = true)] + public int Get() + { + int x = 0; + for (int i = 0; i < SIZE; i++) + { + x = _array[i]; + } + return x; + } + + [Benchmark] + public int UnsafeGet() + { + int x = 0; + for (int i = 0; i < SIZE; i++) + { + x = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_array), i); + } + return x; + } + + [Benchmark] + public int SpanGet() + { + var span = _array.AsSpan(); + int x = 0; + for (int i = 0; i < SIZE; i++) + { + x = span[i]; + } + return x; + } + + [Benchmark] + public int SpanListGet() + { + var span = CollectionsMarshal.AsSpan(_list); + int x = 0; + for (int i = 0; i < SIZE; i++) + { + x = span[i]; + } + return x; + } + + [Benchmark] + public int GetReverse() + { + int x = 0; + for (int i = SIZE - 1; i >= 0; i--) + { + x = _array[i]; + } + + return x; + } +} \ No newline at end of file diff --git a/Benchmarks/BestPractices/Boxing.cs b/Benchmarks/BestPractices/Boxing.cs new file mode 100644 index 0000000..37fcffa --- /dev/null +++ b/Benchmarks/BestPractices/Boxing.cs @@ -0,0 +1,41 @@ +namespace Benchmarks.BestPractices; + +/* +| Method | Runtime | Mean | Error | StdDev | Ratio | Gen0 | Allocated | +|------- |--------- |-----------:|---------:|----------:|---------:|-------:|----------:| +| Boxing | .NET 6.0 | 2,881.4 ns | 55.29 ns | 71.89 ns | baseline | 3.8261 | 24024 B | +| Int | .NET 6.0 | 244.8 ns | 3.10 ns | 2.75 ns | -92% | 0.0038 | 24 B | +| | | | | | | | | +| Boxing | .NET 8.0 | 2,946.5 ns | 56.35 ns | 155.20 ns | baseline | 3.8261 | 24024 B | +| Int | .NET 8.0 | 248.8 ns | 4.66 ns | 4.13 ns | -92% | 0.0038 | 24 B | + */ +public class BoxingBench +{ + private const int SIZE = 1000; + + [Benchmark(Baseline = true)] + public object Boxing() + { + int x = 0; + object obj = x; + for (int i = 0; i < SIZE; i++) + { + x++; + obj = x; + } + return obj; + } + + + [Benchmark] + public object Int() + { + int x = 0; + object obj = x; + for (int i = 0; i < SIZE; i++) + { + x++; + } + return obj; + } +} \ No newline at end of file diff --git a/Benchmarks/BestPractices/BoxingUnsafe.cs b/Benchmarks/BestPractices/BoxingUnsafe.cs new file mode 100644 index 0000000..3e19156 --- /dev/null +++ b/Benchmarks/BestPractices/BoxingUnsafe.cs @@ -0,0 +1,38 @@ +namespace Benchmarks.BestPractices; + +/* +| Method | Runtime | Mean | Error | StdDev | Ratio | Gen0 | Allocated | +|--------------- |--------- |---------:|----------:|----------:|---------:|-------:|----------:|- +| UnBoxing | .NET 6.0 | 2.931 us | 0.0568 us | 0.0584 us | baseline | 3.8261 | 24024 B | +| UnBoxingUnsafe | .NET 6.0 | 1.248 us | 0.0046 us | 0.0041 us | -57% | 0.0038 | 24 B | +| | | | | | | | | +| UnBoxing | .NET 8.0 | 2.961 us | 0.0547 us | 0.0427 us | baseline | 3.8261 | 24024 B | +| UnBoxingUnsafe | .NET 8.0 | 1.236 us | 0.0235 us | 0.0289 us | -58% | 0.0038 | 24 B | + */ +public class BoxingUnsafe +{ + private const int SIZE = 1000; + + [Benchmark(Baseline = true)] + public object UnBoxing() + { + object obj = 0; + for (int i = 0; i < SIZE; i++) + Inc((int)obj); + + return obj; + + static object Inc(int x) => ++x; + } + + + [Benchmark] + public object UnBoxingUnsafe() + { + object obj = 0; + for (int i = 0; i < SIZE; i++) + Unsafe.Unbox(obj)++; + + return obj; + } +} \ No newline at end of file diff --git a/Benchmarks/BestPractices/CodeGen.cs b/Benchmarks/BestPractices/CodeGen.cs new file mode 100644 index 0000000..1938244 --- /dev/null +++ b/Benchmarks/BestPractices/CodeGen.cs @@ -0,0 +1,43 @@ +namespace Benchmarks.BestPractices; +internal class CodeGen +{ + static bool IsEven(int i) => (i % 2) == 0; +} + + +/* +mono: +Program:IsEven(System.Int32):System.Boolean: + sub rsp, 8 + mov [rsp], r15 + mov r15, rdi + mov rcx, r15 + shr ecx, 1Fh + mov rax, r15 + add eax, ecx + and rax, 1 + sub eax, ecx + test eax, eax + sete al + movzx rax, al + mov r15, [rsp] + add rsp, 8 + ret + + 6.0 +Program:IsEven(int):bool: + test dil, 1 + sete al + movzx rax, al + ret + +8.0 +Program:IsEven(int):bool (FullOpts): +G_M20272_IG01: ;; offset=0x0000 +G_M20272_IG02: ;; offset=0x0000 + mov eax, edi + not eax + and eax, 1 +G_M20272_IG03: ;; offset=0x0007 + ret +*/ \ No newline at end of file diff --git a/Benchmarks/BestPractices/Demo.md b/Benchmarks/BestPractices/Demo.md new file mode 100644 index 0000000..ef82885 --- /dev/null +++ b/Benchmarks/BestPractices/Demo.md @@ -0,0 +1,31 @@ +[Start](https://benchmarkdotnet.org/) +[Getting Started Guide](https://benchmarkdotnet.org/articles/guides/getting-started.html) +[Main Features List](https://benchmarkdotnet.org/#main-features) + + + +# Running +[Config](https://benchmarkdotnet.org/articles/configs/configs.html) + +```csharp +var config = ManualConfig + .Create(DefaultConfig.Instance) + .AddValidator(ExecutionValidator.FailOnError) + .WithSummaryStyle(SummaryStyle.Default.WithRatioStyle(RatioStyle.Percentage)) + .AddDiagnoser(MemoryDiagnoser.Default) + .HideColumns("Median", "RatioSD", "Alloc Ratio") //"Error", "StdDev", + .AddJob(Job.Default.WithRuntime(CoreRuntime.Core60)) + .AddJob(Job.Default.WithRuntime(CoreRuntime.Core80)) + ; +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config); +``` + + +# Results +1. [Exporters](https://benchmarkdotnet.org/articles/configs/exporters.html) +2. [Charting](https://chartbenchmark.net/) +3. [Plot for blogs](https://michalbrylka.github.io/posts/generic-math-matrix/#performance) + + +# Utils +1. [BenchmarkInput.cs](Helpers/BenchmarkInput.cs) diff --git a/Benchmarks/BestPractices/GeneratorBench.cs b/Benchmarks/BestPractices/GeneratorBench.cs new file mode 100644 index 0000000..e30214e --- /dev/null +++ b/Benchmarks/BestPractices/GeneratorBench.cs @@ -0,0 +1,142 @@ +namespace Benchmarks.BestPractices; + +/* +| Method | Runtime | Mean | Ratio | +|----------------- |--------- |-----------:|---------:| +| Yield | .NET 6.0 | 3,778.5 ns | baseline | +| YieldLocalMethod | .NET 6.0 | 3,760.8 ns | -0% | +| EnumerableRange | .NET 6.0 | 3,536.0 ns | -6% | +| ArrayBench | .NET 6.0 | 850.7 ns | -77% | +| | | | | +| Yield | .NET 8.0 | 1,452.5 ns | baseline | +| YieldLocalMethod | .NET 8.0 | 1,440.6 ns | -1% | +| EnumerableRange | .NET 8.0 | 1,432.5 ns | -1% | +| ArrayBench | .NET 8.0 | 753.2 ns | -48% | + */ +public class GeneratorBench +{ + private const int SIZE = 1000; + + [Benchmark(Baseline = true)] + public int Yield() + { + int sum = 0; + foreach (var i in Sequence()) + sum += i; + return sum; + } + + static IEnumerable Sequence() + { + for (int i = 0; i < SIZE; i++) + yield return i; + } + + [Benchmark] + public int YieldLocalMethod() + { + int sum = 0; + foreach (var i in SequenceLocal()) + sum += i; + return sum; + + static IEnumerable SequenceLocal() + { + for (int i = 0; i < SIZE; i++) + yield return i; + } + } + + [Benchmark] + public int EnumerableRange() + { + int sum = 0; + foreach (var i in Enumerable.Range(0, SIZE)) + sum += i; + return sum; + } + + [Benchmark] + public int ArrayBench() + { + int sum = 0; + foreach (var i in ArrayGen()) + sum += i; + return sum; + } + + + static int[] ArrayGen() + { + var arr = new int[SIZE]; + for (int i = 0; i < SIZE; i++) + arr[i] = i; + return arr; + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +/* +| Method | Runtime | Mean | Ratio | Gen0 | Allocated | +|----------------- |--------- |-----------:|---------:|-------:|----------:|- +| Yield | .NET 6.0 | 3,778.5 ns | baseline | - | 32 B | +| YieldLocalMethod | .NET 6.0 | 3,760.8 ns | -0% | 0.0038 | 32 B | +| EnumerableRange | .NET 6.0 | 3,536.0 ns | -6% | 0.0038 | 40 B | +| ArrayBench | .NET 6.0 | 850.7 ns | -77% | 0.6409 | 4024 B | +| | | | | | | +| Yield | .NET 8.0 | 1,452.5 ns | baseline | 0.0038 | 32 B | +| YieldLocalMethod | .NET 8.0 | 1,440.6 ns | -1% | 0.0038 | 32 B | +| EnumerableRange | .NET 8.0 | 1,432.5 ns | -1% | 0.0057 | 40 B | +| ArrayBench | .NET 8.0 | 753.2 ns | -48% | 0.6409 | 4024 B | +*/ \ No newline at end of file diff --git a/Benchmarks/BestPractices/Nullable.cs b/Benchmarks/BestPractices/Nullable.cs new file mode 100644 index 0000000..cc9ad46 --- /dev/null +++ b/Benchmarks/BestPractices/Nullable.cs @@ -0,0 +1,34 @@ +namespace Benchmarks.BestPractices; + +/* +| Method | Runtime | Mean | Error | StdDev | Ratio | Allocated | +|--------- |--------- |---------:|---------:|---------:|---------:|----------:|- +| Nullable | .NET 6.0 | 493.3 ns | 9.84 ns | 9.67 ns | baseline | - | +| Int | .NET 6.0 | 244.6 ns | 4.08 ns | 3.62 ns | -50% | - | +| | | | | | | | +| Nullable | .NET 8.0 | 730.6 ns | 13.59 ns | 25.52 ns | baseline | - | +| Int | .NET 8.0 | 243.7 ns | 3.84 ns | 4.72 ns | -67% | - | + */ +public class NullableBench +{ + private const int SIZE = 1000; + + [Benchmark(Baseline = true)] + public int Nullable() + { + int? x = 0; + for (int i = 0; i < SIZE; i++) + x++; + return x.Value; + } + + + [Benchmark] + public int Int() + { + int x = 0; + for (int i = 0; i < SIZE; i++) + x++; + return x; + } +} \ No newline at end of file diff --git a/Benchmarks/Program.cs b/Benchmarks/Program.cs index af56cdd..6588323 100644 --- a/Benchmarks/Program.cs +++ b/Benchmarks/Program.cs @@ -11,7 +11,7 @@ .AddValidator(ExecutionValidator.FailOnError) .WithSummaryStyle(SummaryStyle.Default.WithRatioStyle(RatioStyle.Percentage)) .AddDiagnoser(MemoryDiagnoser.Default) - .HideColumns("Error", "StdDev", "Median", "RatioSD", "Alloc Ratio") + .HideColumns("Median", "RatioSD", "Alloc Ratio") //"Error", "StdDev", .AddJob(Job.Default.WithRuntime(CoreRuntime.Core60)) .AddJob(Job.Default.WithRuntime(CoreRuntime.Core80)) ;