Skip to content

Commit

Permalink
Use optimized hash creation methods on .NET 5+
Browse files Browse the repository at this point in the history
  • Loading branch information
sharwell committed Apr 27, 2023
1 parent 663ceb3 commit 9a5cf21
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 38 deletions.
69 changes: 50 additions & 19 deletions src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,29 @@ internal partial class Checksum
private static readonly ObjectPool<IncrementalHash> s_incrementalHashPool =
new(() => IncrementalHash.CreateHash(HashAlgorithmName.SHA256), size: 20);

#if !NET5_0_OR_GREATER
// Dedicated pools for the byte[]s we use to create checksums from two or three existing checksums. Sized to
// exactly the space needed to splat the existing checksum data into the array and then hash it.

private static readonly ObjectPool<byte[]> s_twoChecksumByteArrayPool = new(() => new byte[HashSize * 2]);
private static readonly ObjectPool<byte[]> s_threeChecksumByteArrayPool = new(() => new byte[HashSize * 3]);
#endif

public static Checksum Create(IEnumerable<string> values)
{
#if NET5_0_OR_GREATER
using var pooledHash = s_incrementalHashPool.GetPooledObject();

foreach (var value in values)
{
pooledHash.Object.AppendData(MemoryMarshal.AsBytes(value.AsSpan()));
pooledHash.Object.AppendData(MemoryMarshal.AsBytes("\0".AsSpan()));
}

Span<byte> hash = stackalloc byte[SHA256HashSizeBytes];
pooledHash.Object.GetHashAndReset(hash);
return From(hash);
#else
using var pooledHash = s_incrementalHashPool.GetPooledObject();
using var pooledBuffer = SharedPools.ByteArray.GetPooledObject();
var hash = pooledHash.Object;
Expand All @@ -45,21 +60,51 @@ public static Checksum Create(IEnumerable<string> values)
}

return From(hash.GetHashAndReset());
#endif
}

public static Checksum Create(string value)
{
#if NET5_0_OR_GREATER
Span<byte> hash = stackalloc byte[SHA256HashSizeBytes];
SHA256.HashData(MemoryMarshal.AsBytes(value.AsSpan()), hash);
return From(hash);
#else
using var pooledHash = s_incrementalHashPool.GetPooledObject();
using var pooledBuffer = SharedPools.ByteArray.GetPooledObject();
var hash = pooledHash.Object;

AppendData(hash, pooledBuffer.Object, value);

return From(hash.GetHashAndReset());
#endif
}

public static Checksum Create(Stream stream)
{
#if NET7_0_OR_GREATER
Span<byte> hash = stackalloc byte[SHA256HashSizeBytes];
SHA256.HashData(stream, hash);
return From(hash);
#elif NET5_0_OR_GREATER
using var pooledHash = s_incrementalHashPool.GetPooledObject();
Span<byte> buffer = stackalloc byte[SharedPools.ByteBufferSize];

int bytesRead;
do
{
bytesRead = stream.Read(buffer);
if (bytesRead > 0)
{
pooledHash.Object.AppendData(buffer[..bytesRead]);
}
}
while (bytesRead > 0);

Span<byte> hash = stackalloc byte[SHA256HashSizeBytes];
pooledHash.Object.GetHashAndReset(hash);
return From(hash);
#else
using var pooledHash = s_incrementalHashPool.GetPooledObject();
using var pooledBuffer = SharedPools.ByteArray.GetPooledObject();

Expand Down Expand Up @@ -91,6 +136,7 @@ public static Checksum Create(Stream stream)
// hash algorithm used here should remain functionally correct even
// after the truncation
return From(bytes);
#endif
}

public static Checksum Create(IObjectWritable @object)
Expand Down Expand Up @@ -124,6 +170,8 @@ public static Checksum Create(Checksum checksum1, Checksum checksum2, Checksum c
#endif
}

#if !NET5_0_OR_GREATER

private static Checksum CreateUsingByteArrays(Checksum checksum1, Checksum checksum2)
{
using var hash = s_incrementalHashPool.GetPooledObject();
Expand Down Expand Up @@ -153,7 +201,7 @@ private static Checksum CreateUsingByteArrays(Checksum checksum1, Checksum check
return From(hash.Object.GetHashAndReset());
}

#if NET
#else

// Optimized helpers that do not need to allocate any arrays to combine hashes.

Expand Down Expand Up @@ -234,6 +282,7 @@ public static Checksum Create(ParseOptions value, ISerializerService serializer)
return Create(stream);
}

#if !NET5_0_OR_GREATER
private static void AppendData(IncrementalHash hash, byte[] buffer, string value)
{
var stringBytes = MemoryMarshal.AsBytes(value.AsSpan());
Expand All @@ -251,24 +300,6 @@ private static void AppendData(IncrementalHash hash, byte[] buffer, string value
index += toCopy;
}
}

public static class TestAccessor
{
public static Checksum CreateUsingByteArrays(Checksum checksum1, Checksum checksum2)
=> Checksum.CreateUsingByteArrays(checksum1, checksum2);

public static Checksum CreateUsingByteArrays(Checksum checksum1, Checksum checksum2, Checksum checksum3)
=> Checksum.CreateUsingByteArrays(checksum1, checksum2, checksum3);

#if NET

public static Checksum CreateUsingSpans(Checksum checksum1, Checksum checksum2)
=> Checksum.CreateUsingSpans(checksum1, checksum2);

public static Checksum CreateUsingSpans(Checksum checksum1, Checksum checksum2, Checksum checksum3)
=> Checksum.CreateUsingSpans(checksum1, checksum2, checksum3);

#endif
}
}
}
26 changes: 7 additions & 19 deletions src/Workspaces/CoreTest/ChecksumTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,27 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.CodeAnalysis.UnitTests
{
public class ChecksumTests
{
#if NET
[Fact]
public void ValidateChecksumFromSpanSameAsChecksumFromBytes1()
{
var checksum1 = Checksum.Create("Goo");
var checksum2 = Checksum.Create("Bar");

var checksumA = Checksum.TestAccessor.CreateUsingByteArrays(checksum1, checksum2);
var checksumB = Checksum.TestAccessor.CreateUsingSpans(checksum1, checksum2);
var checksumA = Checksum.Create(checksum1, checksum2);

Assert.Equal(checksumA, checksumB);
// Running this test on multiple target frameworks with the same expectation ensures the results match
Assert.Equal(Checksum.FromBase64String("N30m5jwVeMZzWpy9cbQbtSYHoXU="), checksumA);

Assert.NotEqual(checksum1, checksum2);

Assert.NotEqual(checksum1, checksumA);
Assert.NotEqual(checksum1, checksumB);
Assert.NotEqual(checksum2, checksumA);
Assert.NotEqual(checksum2, checksumB);
}

[Fact]
Expand All @@ -40,22 +32,18 @@ public void ValidateChecksumFromSpanSameAsChecksumFromBytes2()
var checksum2 = Checksum.Create("Bar");
var checksum3 = Checksum.Create("Baz");

var checksumA = Checksum.TestAccessor.CreateUsingByteArrays(checksum1, checksum2, checksum3);
var checksumB = Checksum.TestAccessor.CreateUsingSpans(checksum1, checksum2, checksum3);
var checksumA = Checksum.Create(checksum1, checksum2, checksum3);

Assert.Equal(checksumA, checksumB);
// Running this test on multiple target frameworks with the same expectation ensures the results match
Assert.Equal(Checksum.FromBase64String("NEfIznmqkIqi4VJl12KxycWt7uo="), checksumA);

Assert.NotEqual(checksum1, checksum2);
Assert.NotEqual(checksum2, checksum3);
Assert.NotEqual(checksum3, checksum1);

Assert.NotEqual(checksum1, checksumA);
Assert.NotEqual(checksum1, checksumB);
Assert.NotEqual(checksum2, checksumA);
Assert.NotEqual(checksum2, checksumB);
Assert.NotEqual(checksum3, checksumA);
Assert.NotEqual(checksum3, checksumB);
}
#endif
}
}

0 comments on commit 9a5cf21

Please sign in to comment.