From 37e88ae198aa8d87094763afce3a5170130e92dd Mon Sep 17 00:00:00 2001 From: Brant Burnett Date: Sat, 17 Oct 2020 00:20:13 -0400 Subject: [PATCH] Add .NET Core 2.1 and 3.0 perf improvements The addition of Span in .NET Core 2.1 can offer some performance improvements moving through the array in SafeProxy by reducing the number of arithmetic operations. .NET Core 3.0 also adds Span based overloads to HashAlgorithm which can further improve performance if explicitly supported. If not supported, any requests to the Span overloads are copied to an array before processing. A BenchmarkDotNet project was also added to assist with benchmarking. Test results across several target frameworks comparing the pre and post change performance against a 65536 byte array. These metrics are for calls in via the array overloads, not the Span overloads. They show an approximately 25% reduction in runtime on .NET Core 2.1 and 3.1. | Method | Runtime | Size | Mean | Error | StdDev | Ratio | Rank | |------- |-------------- |------ |---------:|---------:|---------:|------:|-----:| | Array | .NET 4.6.1 | 65536 | 48.08 us | 0.192 us | 0.170 us | 1.00 | 1 | | Span | .NET 4.6.1 | 65536 | 47.87 us | 0.169 us | 0.150 us | 1.00 | 1 | | | | | | | | | | | Array | .NET Core 2.1 | 65536 | 48.99 us | 0.260 us | 0.217 us | 1.00 | 2 | | Span | .NET Core 2.1 | 65536 | 37.02 us | 0.261 us | 0.218 us | 0.76 | 1 | | | | | | | | | | | Array | .NET Core 3.1 | 65536 | 50.01 us | 0.335 us | 0.297 us | 1.00 | 2 | | Span | .NET Core 3.1 | 65536 | 37.04 us | 0.218 us | 0.204 us | 0.74 | 1 | --- .gitignore | 3 +- .../Crc32.NET.Benchmarks.csproj | 18 ++++ Crc32.NET.Benchmarks/Crc32.cs | 43 +++++++++ Crc32.NET.Benchmarks/Program.cs | 10 +++ Crc32.NET.Benchmarks/StandardConfig.cs | 23 +++++ Crc32.NET.Core.sln | 17 +++- Crc32.NET.Tests/Crc32.NET.Tests.Core.csproj | 5 +- Crc32.NET.Tests/ImplementationTest.cs | 28 +++++- Crc32.NET/Crc32.NET.Core.csproj | 8 +- Crc32.NET/Crc32Algorithm.cs | 90 +++++++++++++++++-- Crc32.NET/Crc32CAlgorithm.cs | 86 +++++++++++++++++- Crc32.NET/SafeProxy.cs | 59 ++++++++++-- 12 files changed, 367 insertions(+), 23 deletions(-) create mode 100644 Crc32.NET.Benchmarks/Crc32.NET.Benchmarks.csproj create mode 100644 Crc32.NET.Benchmarks/Crc32.cs create mode 100644 Crc32.NET.Benchmarks/Program.cs create mode 100644 Crc32.NET.Benchmarks/StandardConfig.cs diff --git a/.gitignore b/.gitignore index 3babd98..f9d4413 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ obj packages .tools private.snk -.idea \ No newline at end of file +.idea +BenchmarkDotNet.Artifacts/ \ No newline at end of file diff --git a/Crc32.NET.Benchmarks/Crc32.NET.Benchmarks.csproj b/Crc32.NET.Benchmarks/Crc32.NET.Benchmarks.csproj new file mode 100644 index 0000000..920970e --- /dev/null +++ b/Crc32.NET.Benchmarks/Crc32.NET.Benchmarks.csproj @@ -0,0 +1,18 @@ + + + + Exe + net461;netcoreapp2.1;netcoreapp3.1 + + false + + + + + + + + + + + diff --git a/Crc32.NET.Benchmarks/Crc32.cs b/Crc32.NET.Benchmarks/Crc32.cs new file mode 100644 index 0000000..c177a49 --- /dev/null +++ b/Crc32.NET.Benchmarks/Crc32.cs @@ -0,0 +1,43 @@ +using System; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using Force.Crc32; + +namespace Crc32.NET.Benchmarks +{ + [SimpleJob(RuntimeMoniker.NetCoreApp31)] + public class Crc32 + { + private byte[] _input; + private byte[] _destination; + + [Params(65536)] + public int Size { get; set; } + + [GlobalSetup] + public void Setup() + { + _input = new byte[Size]; + var random = new Random(); + random.NextBytes(_input); + + _destination = new byte[4]; + } + + [Benchmark(Baseline = true)] + public byte[] Array() + { + var crc = new Crc32Algorithm(); + return crc.ComputeHash(_input); + } + +#if NETCOREAPP3_1 + [Benchmark] + public void Span() + { + var crc = new Crc32Algorithm(); + crc.TryComputeHash(_input.AsSpan(), _destination.AsSpan(), out _); + } +#endif + } +} diff --git a/Crc32.NET.Benchmarks/Program.cs b/Crc32.NET.Benchmarks/Program.cs new file mode 100644 index 0000000..452ff95 --- /dev/null +++ b/Crc32.NET.Benchmarks/Program.cs @@ -0,0 +1,10 @@ +using System; +using BenchmarkDotNet.Running; + +namespace Crc32.NET.Benchmarks +{ + class Program + { + static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, new StandardConfig()); + } +} diff --git a/Crc32.NET.Benchmarks/StandardConfig.cs b/Crc32.NET.Benchmarks/StandardConfig.cs new file mode 100644 index 0000000..b1e6538 --- /dev/null +++ b/Crc32.NET.Benchmarks/StandardConfig.cs @@ -0,0 +1,23 @@ +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Loggers; + +namespace Crc32.NET.Benchmarks +{ + public class StandardConfig : ManualConfig + { + public StandardConfig() + { + AddColumnProvider(DefaultColumnProviders.Instance); + AddColumn(RankColumn.Arabic); + + AddExporter(DefaultExporters.CsvMeasurements); + AddExporter(DefaultExporters.Csv); + AddExporter(DefaultExporters.Markdown); + AddExporter(DefaultExporters.Html); + + AddLogger(ConsoleLogger.Default); + } + } +} diff --git a/Crc32.NET.Core.sln b/Crc32.NET.Core.sln index 51e72af..5bdbb30 100644 --- a/Crc32.NET.Core.sln +++ b/Crc32.NET.Core.sln @@ -1,17 +1,19 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26114.2 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30611.23 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Crc32.NET", "Crc32.NET\Crc32.NET.Core.csproj", "{E95FA3F3-4ED0-41FF-9A1F-DE80CE14A976}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Crc32.NET.Core", "Crc32.NET\Crc32.NET.Core.csproj", "{E95FA3F3-4ED0-41FF-9A1F-DE80CE14A976}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Crc32.NET.Tests", "Crc32.NET.Tests\Crc32.NET.Tests.Core.csproj", "{A602A9CA-793A-4096-A93C-799CA74519BF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Crc32.NET.Tests.Core", "Crc32.NET.Tests\Crc32.NET.Tests.Core.csproj", "{A602A9CA-793A-4096-A93C-799CA74519BF}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{56B92A80-3AE8-4FD0-B1F6-3847919216DA}" ProjectSection(SolutionItems) = preProject README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Crc32.NET.Benchmarks", "Crc32.NET.Benchmarks\Crc32.NET.Benchmarks.csproj", "{13C3DDAD-50F1-4804-95A1-D54766EA1247}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,8 +28,15 @@ Global {A602A9CA-793A-4096-A93C-799CA74519BF}.Debug|Any CPU.Build.0 = Debug|Any CPU {A602A9CA-793A-4096-A93C-799CA74519BF}.Release|Any CPU.ActiveCfg = Release|Any CPU {A602A9CA-793A-4096-A93C-799CA74519BF}.Release|Any CPU.Build.0 = Release|Any CPU + {13C3DDAD-50F1-4804-95A1-D54766EA1247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13C3DDAD-50F1-4804-95A1-D54766EA1247}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13C3DDAD-50F1-4804-95A1-D54766EA1247}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13C3DDAD-50F1-4804-95A1-D54766EA1247}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A1461F74-929C-4C81-8E25-B44498AEEF6D} + EndGlobalSection EndGlobal diff --git a/Crc32.NET.Tests/Crc32.NET.Tests.Core.csproj b/Crc32.NET.Tests/Crc32.NET.Tests.Core.csproj index abd6655..699d45f 100644 --- a/Crc32.NET.Tests/Crc32.NET.Tests.Core.csproj +++ b/Crc32.NET.Tests/Crc32.NET.Tests.Core.csproj @@ -1,6 +1,6 @@  - netcoreapp1.0;netcoreapp2.0;net461 + netcoreapp1.0;netcoreapp2.0;netcoreapp3.1;net461 portable Crc32.NET.Tests Exe @@ -40,6 +40,9 @@ $(DefineConstants);NETCORE;NETCORE20 + + $(DefineConstants);NETCORE;NETCORE30 + $(DefineConstants);COREVERSION diff --git a/Crc32.NET.Tests/ImplementationTest.cs b/Crc32.NET.Tests/ImplementationTest.cs index 7bfe6da..2ff5fb9 100644 --- a/Crc32.NET.Tests/ImplementationTest.cs +++ b/Crc32.NET.Tests/ImplementationTest.cs @@ -1,4 +1,7 @@ using System; +#if NETCORE30 +using System.Buffers.Binary; +#endif using System.Linq; using System.Text; @@ -30,7 +33,7 @@ public void ResultConsistency(string text, int offset) Assert.That(crc2, Is.EqualTo(crc1)); } #endif - + [Test] public void ResultConsistency2() { @@ -52,7 +55,28 @@ public void ResultConsistencyAsHashAlgorithm() Console.WriteLine(crc2.ToString("X8")); Assert.That(crc1, Is.EqualTo(crc2)); } -#endif +#endif + +#if NETCORE30 + [Test] + public void ResultConsistencyAsHashAlgorithm_SpanVsArray() + { + var bytes = new byte[30000]; + new Random().NextBytes(bytes); + var e = new Crc32Algorithm(); + var c = new Crc32Algorithm(); + var crc1 = BitConverter.ToInt32(e.ComputeHash(bytes), 0); + + var dest = new byte[4]; + Assert.That(c.TryComputeHash(bytes, dest, out var bytesWritten), Is.True); + Assert.That(bytesWritten, Is.EqualTo(4)); + var crc2 = BinaryPrimitives.ReadInt32LittleEndian(dest); + + Console.WriteLine(crc1.ToString("X8")); + Console.WriteLine(crc2.ToString("X8")); + Assert.That(crc1, Is.EqualTo(crc2)); + } +#endif [Test] public void PartIsWhole() diff --git a/Crc32.NET/Crc32.NET.Core.csproj b/Crc32.NET/Crc32.NET.Core.csproj index cec1661..a2c3035 100644 --- a/Crc32.NET/Crc32.NET.Core.csproj +++ b/Crc32.NET/Crc32.NET.Core.csproj @@ -1,7 +1,7 @@  1.2.1 - netstandard1.3;netstandard2.0;net20 + netstandard1.3;netstandard2.0;netstandard2.1;net20 true Crc32.NET