Skip to content

Commit 37e88ae

Browse files
committed
Add .NET Core 2.1 and 3.0 perf improvements
The addition of Span<T> 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<byte> based overloads to HashAlgorithm which can further improve performance if explicitly supported. If not supported, any requests to the Span<byte> 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<byte> 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 |
1 parent fbc1061 commit 37e88ae

12 files changed

Lines changed: 367 additions & 23 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ obj
55
packages
66
.tools
77
private.snk
8-
.idea
8+
.idea
9+
BenchmarkDotNet.Artifacts/
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFrameworks>net461;netcoreapp2.1;netcoreapp3.1</TargetFrameworks>
6+
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\Crc32.NET\Crc32.NET.Core.csproj" />
16+
</ItemGroup>
17+
18+
</Project>

Crc32.NET.Benchmarks/Crc32.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using BenchmarkDotNet.Attributes;
3+
using BenchmarkDotNet.Jobs;
4+
using Force.Crc32;
5+
6+
namespace Crc32.NET.Benchmarks
7+
{
8+
[SimpleJob(RuntimeMoniker.NetCoreApp31)]
9+
public class Crc32
10+
{
11+
private byte[] _input;
12+
private byte[] _destination;
13+
14+
[Params(65536)]
15+
public int Size { get; set; }
16+
17+
[GlobalSetup]
18+
public void Setup()
19+
{
20+
_input = new byte[Size];
21+
var random = new Random();
22+
random.NextBytes(_input);
23+
24+
_destination = new byte[4];
25+
}
26+
27+
[Benchmark(Baseline = true)]
28+
public byte[] Array()
29+
{
30+
var crc = new Crc32Algorithm();
31+
return crc.ComputeHash(_input);
32+
}
33+
34+
#if NETCOREAPP3_1
35+
[Benchmark]
36+
public void Span()
37+
{
38+
var crc = new Crc32Algorithm();
39+
crc.TryComputeHash(_input.AsSpan(), _destination.AsSpan(), out _);
40+
}
41+
#endif
42+
}
43+
}

Crc32.NET.Benchmarks/Program.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System;
2+
using BenchmarkDotNet.Running;
3+
4+
namespace Crc32.NET.Benchmarks
5+
{
6+
class Program
7+
{
8+
static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, new StandardConfig());
9+
}
10+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using BenchmarkDotNet.Columns;
2+
using BenchmarkDotNet.Configs;
3+
using BenchmarkDotNet.Exporters;
4+
using BenchmarkDotNet.Loggers;
5+
6+
namespace Crc32.NET.Benchmarks
7+
{
8+
public class StandardConfig : ManualConfig
9+
{
10+
public StandardConfig()
11+
{
12+
AddColumnProvider(DefaultColumnProviders.Instance);
13+
AddColumn(RankColumn.Arabic);
14+
15+
AddExporter(DefaultExporters.CsvMeasurements);
16+
AddExporter(DefaultExporters.Csv);
17+
AddExporter(DefaultExporters.Markdown);
18+
AddExporter(DefaultExporters.Html);
19+
20+
AddLogger(ConsoleLogger.Default);
21+
}
22+
}
23+
}

Crc32.NET.Core.sln

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio 15
4-
VisualStudioVersion = 15.0.26114.2
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.30611.23
55
MinimumVisualStudioVersion = 10.0.40219.1
6-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Crc32.NET", "Crc32.NET\Crc32.NET.Core.csproj", "{E95FA3F3-4ED0-41FF-9A1F-DE80CE14A976}"
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Crc32.NET.Core", "Crc32.NET\Crc32.NET.Core.csproj", "{E95FA3F3-4ED0-41FF-9A1F-DE80CE14A976}"
77
EndProject
8-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Crc32.NET.Tests", "Crc32.NET.Tests\Crc32.NET.Tests.Core.csproj", "{A602A9CA-793A-4096-A93C-799CA74519BF}"
8+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Crc32.NET.Tests.Core", "Crc32.NET.Tests\Crc32.NET.Tests.Core.csproj", "{A602A9CA-793A-4096-A93C-799CA74519BF}"
99
EndProject
1010
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{56B92A80-3AE8-4FD0-B1F6-3847919216DA}"
1111
ProjectSection(SolutionItems) = preProject
1212
README.md = README.md
1313
EndProjectSection
1414
EndProject
15+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Crc32.NET.Benchmarks", "Crc32.NET.Benchmarks\Crc32.NET.Benchmarks.csproj", "{13C3DDAD-50F1-4804-95A1-D54766EA1247}"
16+
EndProject
1517
Global
1618
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1719
Debug|Any CPU = Debug|Any CPU
@@ -26,8 +28,15 @@ Global
2628
{A602A9CA-793A-4096-A93C-799CA74519BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
2729
{A602A9CA-793A-4096-A93C-799CA74519BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
2830
{A602A9CA-793A-4096-A93C-799CA74519BF}.Release|Any CPU.Build.0 = Release|Any CPU
31+
{13C3DDAD-50F1-4804-95A1-D54766EA1247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32+
{13C3DDAD-50F1-4804-95A1-D54766EA1247}.Debug|Any CPU.Build.0 = Debug|Any CPU
33+
{13C3DDAD-50F1-4804-95A1-D54766EA1247}.Release|Any CPU.ActiveCfg = Release|Any CPU
34+
{13C3DDAD-50F1-4804-95A1-D54766EA1247}.Release|Any CPU.Build.0 = Release|Any CPU
2935
EndGlobalSection
3036
GlobalSection(SolutionProperties) = preSolution
3137
HideSolutionNode = FALSE
3238
EndGlobalSection
39+
GlobalSection(ExtensibilityGlobals) = postSolution
40+
SolutionGuid = {A1461F74-929C-4C81-8E25-B44498AEEF6D}
41+
EndGlobalSection
3342
EndGlobal

Crc32.NET.Tests/Crc32.NET.Tests.Core.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks>netcoreapp1.0;netcoreapp2.0;net461</TargetFrameworks>
3+
<TargetFrameworks>netcoreapp1.0;netcoreapp2.0;netcoreapp3.1;net461</TargetFrameworks>
44
<DebugType>portable</DebugType>
55
<AssemblyName>Crc32.NET.Tests</AssemblyName>
66
<OutputType>Exe</OutputType>
@@ -40,6 +40,9 @@
4040
<PropertyGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">
4141
<DefineConstants>$(DefineConstants);NETCORE;NETCORE20</DefineConstants>
4242
</PropertyGroup>
43+
<PropertyGroup Condition=" '$(TargetFramework)' == 'netcoreapp3.1' ">
44+
<DefineConstants>$(DefineConstants);NETCORE;NETCORE30</DefineConstants>
45+
</PropertyGroup>
4346
<PropertyGroup>
4447
<DefineConstants>$(DefineConstants);COREVERSION</DefineConstants>
4548
</PropertyGroup>

Crc32.NET.Tests/ImplementationTest.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using System;
2+
#if NETCORE30
3+
using System.Buffers.Binary;
4+
#endif
25
using System.Linq;
36
using System.Text;
47

@@ -30,7 +33,7 @@ public void ResultConsistency(string text, int offset)
3033
Assert.That(crc2, Is.EqualTo(crc1));
3134
}
3235
#endif
33-
36+
3437
[Test]
3538
public void ResultConsistency2()
3639
{
@@ -52,7 +55,28 @@ public void ResultConsistencyAsHashAlgorithm()
5255
Console.WriteLine(crc2.ToString("X8"));
5356
Assert.That(crc1, Is.EqualTo(crc2));
5457
}
55-
#endif
58+
#endif
59+
60+
#if NETCORE30
61+
[Test]
62+
public void ResultConsistencyAsHashAlgorithm_SpanVsArray()
63+
{
64+
var bytes = new byte[30000];
65+
new Random().NextBytes(bytes);
66+
var e = new Crc32Algorithm();
67+
var c = new Crc32Algorithm();
68+
var crc1 = BitConverter.ToInt32(e.ComputeHash(bytes), 0);
69+
70+
var dest = new byte[4];
71+
Assert.That(c.TryComputeHash(bytes, dest, out var bytesWritten), Is.True);
72+
Assert.That(bytesWritten, Is.EqualTo(4));
73+
var crc2 = BinaryPrimitives.ReadInt32LittleEndian(dest);
74+
75+
Console.WriteLine(crc1.ToString("X8"));
76+
Console.WriteLine(crc2.ToString("X8"));
77+
Assert.That(crc1, Is.EqualTo(crc2));
78+
}
79+
#endif
5680

5781
[Test]
5882
public void PartIsWhole()

Crc32.NET/Crc32.NET.Core.csproj

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<VersionPrefix>1.2.1</VersionPrefix>
4-
<TargetFrameworks>netstandard1.3;netstandard2.0;net20</TargetFrameworks>
4+
<TargetFrameworks>netstandard1.3;netstandard2.0;netstandard2.1;net20</TargetFrameworks>
55
<GenerateDocumentationFile>true</GenerateDocumentationFile>
66
<AssemblyName>Crc32.NET</AssemblyName>
77
<!--DelaySign>true</DelaySign>
@@ -29,7 +29,13 @@
2929
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
3030
<DefineConstants>$(DefineConstants);NETCORE;NETCORE20</DefineConstants>
3131
</PropertyGroup>
32+
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
33+
<DefineConstants>$(DefineConstants);NETCORE;NETCORE30</DefineConstants>
34+
</PropertyGroup>
3235
<ItemGroup Condition=" '$(TargetFramework)' == 'net20' ">
3336
<Reference Include="System" />
3437
</ItemGroup>
38+
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
39+
<PackageReference Include="System.Memory" Version="4.5.4" />
40+
</ItemGroup>
3541
</Project>

Crc32.NET/Crc32Algorithm.cs

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using System;
2+
#if NETCORE30
3+
using System.Buffers.Binary;
4+
#endif
25
using System.Security.Cryptography;
36

47
namespace Force.Crc32
@@ -14,7 +17,7 @@ public class Crc32Algorithm : HashAlgorithm
1417
private readonly bool _isBigEndian = true;
1518

1619
/// <summary>
17-
/// Initializes a new instance of the <see cref="Crc32Algorithm"/> class.
20+
/// Initializes a new instance of the <see cref="Crc32Algorithm"/> class.
1821
/// </summary>
1922
public Crc32Algorithm()
2023
{
@@ -24,7 +27,7 @@ public Crc32Algorithm()
2427
}
2528

2629
/// <summary>
27-
/// Initializes a new instance of the <see cref="Crc32Algorithm"/> class.
30+
/// Initializes a new instance of the <see cref="Crc32Algorithm"/> class.
2831
/// </summary>
2932
/// <param name="isBigEndian">Should return bytes result as big endian or little endian</param>
3033
// Crc32 by dariogriffo uses big endian, so, we need to be compatible and return big endian as default
@@ -72,6 +75,23 @@ public static uint Append(uint initial, byte[] input)
7275
return AppendInternal(initial, input, 0, input.Length);
7376
}
7477

78+
#if NETCORE20 || NETCORE30
79+
/// <summary>
80+
/// Computes CRC-32 from multiple buffers.
81+
/// Call this method multiple times to chain multiple buffers.
82+
/// </summary>
83+
/// <param name="initial">
84+
/// Initial CRC value for the algorithm. It is zero for the first buffer.
85+
/// Subsequent buffers should have their initial value set to CRC value returned by previous call to this method.
86+
/// </param>
87+
/// <param name="input">Input buffer containing data to be checksummed.</param>
88+
/// <returns>Accumulated CRC-32 of all buffers processed so far.</returns>
89+
public static uint Append(uint initial, ReadOnlySpan<byte> input)
90+
{
91+
return AppendInternal(initial, input);
92+
}
93+
#endif
94+
7595
/// <summary>
7696
/// Computes CRC-32 from input buffer.
7797
/// </summary>
@@ -94,6 +114,18 @@ public static uint Compute(byte[] input)
94114
return Append(0, input);
95115
}
96116

117+
#if NETCORE20 || NETCORE30
118+
/// <summary>
119+
/// Computes CRC-32 from input buffer.
120+
/// </summary>
121+
/// <param name="input">Input buffer with data to be checksummed.</param>
122+
/// <returns>CRC-32 of the data in the buffer.</returns>
123+
public static uint Compute(ReadOnlySpan<byte> input)
124+
{
125+
return Append(0, input);
126+
}
127+
#endif
128+
97129
/// <summary>
98130
/// Computes CRC-32 from input buffer and writes it after end of data (buffer should have 4 bytes reserved space for it). Can be used in conjunction with <see cref="IsValidWithCrcAtEnd(byte[],int,int)"/>
99131
/// </summary>
@@ -162,12 +194,22 @@ public override void Initialize()
162194
/// Appends CRC-32 from given buffer
163195
/// </summary>
164196
protected override void HashCore(byte[] input, int offset, int length)
165-
{
197+
{
166198
_currentCrc = AppendInternal(_currentCrc, input, offset, length);
167199
}
168200

169-
/// <summary>
170-
/// Computes CRC-32 from <see cref="HashCore"/>
201+
#if NETCORE30
202+
/// <summary>
203+
/// Appends CRC-32 from given buffer
204+
/// </summary>
205+
protected override void HashCore(ReadOnlySpan<byte> source)
206+
{
207+
_currentCrc = AppendInternal(_currentCrc, source);
208+
}
209+
#endif
210+
211+
/// <summary>
212+
/// Computes CRC-32 from <see cref="HashCore(byte[], int, int)"/>
171213
/// </summary>
172214
protected override byte[] HashFinal()
173215
{
@@ -177,6 +219,32 @@ protected override byte[] HashFinal()
177219
return new[] { (byte)_currentCrc, (byte)(_currentCrc >> 8), (byte)(_currentCrc >> 16), (byte)(_currentCrc >> 24) };
178220
}
179221

222+
#if NETCORE30
223+
/// <summary>
224+
/// Computes CRC-32 from <see cref="HashCore(ReadOnlySpan{byte})"/>
225+
/// </summary>
226+
protected override bool TryHashFinal(Span<byte> destination, out int bytesWritten)
227+
{
228+
if (destination.Length < 4)
229+
{
230+
bytesWritten = 0;
231+
return false;
232+
}
233+
234+
if (_isBigEndian)
235+
{
236+
BinaryPrimitives.WriteUInt32BigEndian(destination, _currentCrc);
237+
}
238+
else
239+
{
240+
BinaryPrimitives.WriteUInt32LittleEndian(destination, _currentCrc);
241+
}
242+
243+
bytesWritten = 4;
244+
return true;
245+
}
246+
#endif
247+
180248
private static readonly SafeProxy _proxy = new SafeProxy();
181249

182250
private static uint AppendInternal(uint initial, byte[] input, int offset, int length)
@@ -188,5 +256,17 @@ private static uint AppendInternal(uint initial, byte[] input, int offset, int l
188256
else
189257
return initial;
190258
}
259+
260+
#if NETCORE20 || NETCORE30
261+
private static uint AppendInternal(uint initial, ReadOnlySpan<byte> input)
262+
{
263+
if (input.Length > 0)
264+
{
265+
return _proxy.Append(initial, input);
266+
}
267+
else
268+
return initial;
269+
}
270+
#endif
191271
}
192272
}

0 commit comments

Comments
 (0)