Skip to content

Commit 176bc8a

Browse files
27% Faster hashing with less memory allocation.
1 parent 316831a commit 176bc8a

File tree

7 files changed

+58
-34
lines changed

7 files changed

+58
-34
lines changed

Directory.Build.props

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@
1313
<!-- Default settings that are used by other settings -->
1414
<PropertyGroup>
1515
<BaseArtifactsPath>$(MSBuildThisFileDirectory)artifacts/</BaseArtifactsPath>
16-
<BaseArtifactsPathSuffix>$(ImageSharpProjectCategory)/$(MSBuildProjectName)</BaseArtifactsPathSuffix>
16+
<BaseArtifactsPathSuffix>$(SixLaborsProjectCategory)/$(MSBuildProjectName)</BaseArtifactsPathSuffix>
1717
<RepositoryUrl Condition="'$(RepositoryUrl)' == ''">https://github.com/SixLabors/ImageSharp.Web/</RepositoryUrl>
1818
</PropertyGroup>
1919

2020
<!-- Default settings that explicitly differ from the Sdk.props defaults -->
2121
<PropertyGroup>
2222
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
23+
<BaseIntermediateOutputPath>$(BaseArtifactsPath)obj/$(BaseArtifactsPathSuffix)/</BaseIntermediateOutputPath>
2324
<DebugType>portable</DebugType>
2425
<DebugType Condition="'$(codecov)' != ''">full</DebugType>
2526
<NullableContextOptions>disable</NullableContextOptions>
@@ -58,6 +59,7 @@
5859
<!-- Default settings that explicitly differ from the Sdk.targets defaults-->
5960
<PropertyGroup>
6061
<Authors>Six Labors and contributors</Authors>
62+
<BaseOutputPath>$(BaseArtifactsPath)bin/$(BaseArtifactsPathSuffix)/</BaseOutputPath>
6163
<Company>Six Labors</Company>
6264
<PackageOutputPath>$(BaseArtifactsPath)pkg/$(BaseArtifactsPathSuffix)/$(Configuration)/</PackageOutputPath>
6365
<Product>SixLabors.ImageSharp.Web</Product>

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
<!-- DynamicProxyGenAssembly2 is needed so Moq can use our internals -->
4444
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" PublicKey="0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />
4545
<InternalsVisibleTo Include="SixLabors.ImageSharp.Web.Tests" PublicKey="$(SixLaborsPublicKey)" />
46-
<InternalsVisibleTo Include="SixLabors.ImageSharp.Web.Benchmarks" PublicKey="$(SixLaborsPublicKey)" />
46+
<InternalsVisibleTo Include="ImageSharp.Web.Benchmarks" PublicKey="$(SixLaborsPublicKey)" />
4747
</ItemGroup>
4848

4949
</Project>

src/ImageSharp.Web/Caching/CacheHash.cs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
5+
using System.Runtime.CompilerServices;
56
using System.Security.Cryptography;
67
using System.Text;
78
using Microsoft.Extensions.Options;
@@ -33,17 +34,34 @@ public CacheHash(IOptions<ImageSharpMiddlewareOptions> options, MemoryAllocator
3334
}
3435

3536
/// <inheritdoc/>
37+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
3638
public string Create(string value, uint length)
3739
{
38-
int len = (int)length;
3940
int byteCount = Encoding.ASCII.GetByteCount(value);
40-
using (var hashAlgorithm = SHA256.Create())
41-
using (IManagedByteBuffer buffer = this.memoryAllocator.AllocateManagedByteBuffer(byteCount))
41+
42+
// Allocating a buffer from the pool is ~27% slower than stackalloc so use
43+
// that for short strings
44+
if (byteCount < 257)
4245
{
43-
Encoding.ASCII.GetBytes(value, 0, byteCount, buffer.Array, 0);
44-
byte[] hash = hashAlgorithm.ComputeHash(buffer.Array, 0, byteCount);
45-
return $"{HexEncoder.Encode(new Span<byte>(hash).Slice(0, len / 2))}";
46+
return HashValue(value, length, stackalloc byte[byteCount]);
4647
}
48+
49+
using IManagedByteBuffer buffer = this.memoryAllocator.AllocateManagedByteBuffer(byteCount);
50+
return HashValue(value, length, buffer.Memory.Span);
51+
}
52+
53+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
54+
private static string HashValue(string value, uint length, Span<byte> bufferSpan)
55+
{
56+
using var hashAlgorithm = SHA256.Create();
57+
Encoding.ASCII.GetBytes(value, bufferSpan);
58+
59+
// Hashed output maxes out at 32 bytes @ 256bit/8 so we're safe to use stackalloc.
60+
Span<byte> hash = stackalloc byte[32];
61+
hashAlgorithm.TryComputeHash(bufferSpan, hash, out int _);
62+
63+
// length maxes out at 64 since we throw if options is greater.
64+
return HexEncoder.Encode(hash.Slice(0, (int)(length / 2)));
4765
}
4866
}
4967
}

src/ImageSharp.Web/Caching/HexEncoder.cs

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
@@ -28,24 +28,28 @@ internal static class HexEncoder
2828
/// <param name="bytes">The bytes.</param>
2929
/// <returns>The <see cref="string"/>.</returns>
3030
[MethodImpl(MethodImplOptions.AggressiveInlining)]
31-
public static string Encode(Span<byte> bytes)
31+
public static unsafe string Encode(ReadOnlySpan<byte> bytes)
3232
{
33-
int length = bytes.Length;
34-
char[] chars = new char[length * 2];
35-
ref char charRef = ref MemoryMarshal.GetReference<char>(chars);
36-
ref byte bytesRef = ref MemoryMarshal.GetReference(bytes);
37-
ref char hiRef = ref MemoryMarshal.GetReference<char>(HexLutHi);
38-
ref char lowRef = ref MemoryMarshal.GetReference<char>(HexLutLo);
39-
40-
int index = 0;
41-
for (int i = 0; i < length; i++)
33+
fixed (byte* bytesPtr = bytes)
4234
{
43-
byte byteIndex = Unsafe.Add(ref bytesRef, i);
44-
Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref hiRef, byteIndex);
45-
Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref lowRef, byteIndex);
46-
}
35+
return string.Create(bytes.Length * 2, (Ptr: (IntPtr)bytesPtr, bytes.Length), (chars, args) =>
36+
{
37+
var ros = new ReadOnlySpan<byte>((byte*)args.Ptr, args.Length);
4738

48-
return new string(chars, 0, chars.Length);
39+
ref char charRef = ref MemoryMarshal.GetReference(chars);
40+
ref byte bytesRef = ref MemoryMarshal.GetReference(ros);
41+
ref char hiRef = ref MemoryMarshal.GetReference<char>(HexLutHi);
42+
ref char lowRef = ref MemoryMarshal.GetReference<char>(HexLutLo);
43+
44+
int index = 0;
45+
for (int i = 0; i < ros.Length; i++)
46+
{
47+
byte byteIndex = Unsafe.Add(ref bytesRef, i);
48+
Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref hiRef, byteIndex);
49+
Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref lowRef, byteIndex);
50+
}
51+
});
52+
}
4953
}
5054
}
51-
}
55+
}

tests/ImageSharp.Web.Benchmarks/CacheHashBaseline.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ public class CacheHashBaseline : ICacheHash
1616
/// <inheritdoc/>
1717
public string Create(string value, uint length)
1818
{
19-
if (length.CompareTo(2) < 0 || length.CompareTo(64) > 0)
20-
{
21-
throw new ArgumentOutOfRangeException(nameof(length), $"Value must be greater than or equal to {2} and less than or equal to {64}.");
22-
}
23-
19+
// Don't use in benchmark.
20+
// if (length.CompareTo(2) < 0 || length.CompareTo(64) > 0)
21+
// {
22+
// throw new ArgumentOutOfRangeException(nameof(length), $"Value must be greater than or equal to {2} and less than or equal to {64}.");
23+
// }
2424
using (var hashAlgorithm = SHA256.Create())
2525
{
2626
// Concatenate the hash bytes into one long string.

tests/ImageSharp.Web.Benchmarks/Caching/CacheHashBenchmarks.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ namespace SixLabors.ImageSharp.Web.Benchmarks.Caching
1212
public class CacheHashBenchmarks
1313
{
1414
private const string URL = "http://testwebsite.com/image-12345.jpeg?width=400";
15-
private static readonly IOptions<ImageSharpMiddlewareOptions> Options = Microsoft.Extensions.Options.Options.Create(new ImageSharpMiddlewareOptions());
16-
private static readonly CacheHash Sha256Hasher = new CacheHash(Options, Options.Value.Configuration.MemoryAllocator);
15+
private static readonly IOptions<ImageSharpMiddlewareOptions> MWOptions = Options.Create(new ImageSharpMiddlewareOptions());
16+
private static readonly CacheHash Sha256Hasher = new CacheHash(MWOptions, MWOptions.Value.Configuration.MemoryAllocator);
1717
private static readonly CacheHashBaseline NaiveSha256Hasher = new CacheHashBaseline();
1818

1919
[Benchmark(Baseline = true, Description = "Baseline Sha256Hasher")]

tests/ImageSharp.Web.Benchmarks/ImageSharp.Web.Benchmarks.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
55
<TargetFrameworks>netcoreapp3.1;netcoreapp2.1</TargetFrameworks>
6-
<AssemblyName>SixLabors.ImageSharp.Web.Benchmarks</AssemblyName>
6+
<AssemblyName>ImageSharp.Web.Benchmarks</AssemblyName>
77
<RootNamespace>SixLabors.ImageSharp.Web.Benchmarks</RootNamespace>
88
<GenerateProgramFile>false</GenerateProgramFile>
99
<!--Used to hide test project from dotnet test-->

0 commit comments

Comments
 (0)