Optimize Guid.Combine and HexString generation.

This commit is contained in:
James Jackson-South
2018-11-12 16:22:33 +00:00
parent bee1cd571a
commit a8fc62cf42
11 changed files with 357 additions and 56 deletions

View File

@@ -1,39 +0,0 @@
namespace Umbraco.Core
{
public static class ByteArrayExtensions
{
private static readonly char[] BytesToHexStringLookup = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
public static string ToHexString(this byte[] bytes)
{
int i = 0, p = 0, bytesLength = bytes.Length;
var chars = new char[bytesLength * 2];
while (i < bytesLength)
{
var b = bytes[i++];
chars[p++] = BytesToHexStringLookup[b / 0x10];
chars[p++] = BytesToHexStringLookup[b % 0x10];
}
return new string(chars, 0, chars.Length);
}
public static string ToHexString(this byte[] bytes, char separator, int blockSize, int blockCount)
{
int p = 0, bytesLength = bytes.Length, count = 0, size = 0;
var chars = new char[bytesLength * 2 + blockCount];
for (var i = 0; i < bytesLength; i++)
{
var b = bytes[i++];
chars[p++] = BytesToHexStringLookup[b / 0x10];
chars[p++] = BytesToHexStringLookup[b % 0x10];
if (count == blockCount) continue;
if (++size < blockSize) continue;
chars[p++] = '/';
size = 0;
count++;
}
return new string(chars, 0, chars.Length);
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Umbraco.Core
{
/// <summary>
/// Utility methods for the <see cref="Guid"/> struct.
/// </summary>
internal static class GuidUtils
{
/// <summary>
/// Combines two guid instances utilizing an exclusive disjunction.
/// The resultant guid is not guaranteed to be unique since the number of unique bits is halved.
/// </summary>
/// <param name="a">The first guid.</param>
/// <param name="b">The seconds guid.</param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Guid Combine(Guid a, Guid b)
{
var ad = new DecomposedGuid(a);
var bd = new DecomposedGuid(b);
ad.Hi ^= bd.Hi;
ad.Lo ^= bd.Lo;
return ad.Value;
}
/// <summary>
/// A decomposed guid. Allows access to the high and low bits without unsafe code.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
private struct DecomposedGuid
{
[FieldOffset(00)] public Guid Value;
[FieldOffset(00)] public long Hi;
[FieldOffset(08)] public long Lo;
public DecomposedGuid(Guid value) : this() => this.Value = value;
}
}
}

View File

@@ -0,0 +1,84 @@
using System.Linq;
using System.Runtime.CompilerServices;
namespace Umbraco.Core
{
/// <summary>
/// Provides methods for encoding byte arrays into hexidecimal strings.
/// </summary>
internal static class HexEncoder
{
// LUT's that provide the hexidecimal representation of each possible byte value.
private static readonly char[] HexLutBase = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
// The base LUT arranged in 16x each item order. 0 * 16, 1 * 16, .... F * 16
private static readonly char[] HexLutHi = Enumerable.Range(0, 256).Select(x => HexLutBase[x / 0x10]).ToArray();
// The base LUT repeated 16x.
private static readonly char[] HexLutLo = Enumerable.Range(0, 256).Select(x => HexLutBase[x % 0x10]).ToArray();
/// <summary>
/// Converts a <see cref="T:byte[]"/> to a hexidecimal formatted <see cref="string"/> padded to 2 digits.
/// </summary>
/// <param name="bytes">The bytes.</param>
/// <returns>The <see cref="string"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Encode(byte[] bytes)
{
var length = bytes.Length;
var chars = new char[length * 2];
var index = 0;
for (var i = 0; i < length; i++)
{
var byteIndex = bytes[i];
chars[index++] = HexLutHi[byteIndex];
chars[index++] = HexLutLo[byteIndex];
}
return new string(chars, 0, chars.Length);
}
/// <summary>
/// Converts a <see cref="T:byte[]"/> to a hexidecimal formatted <see cref="string"/> padded to 2 digits
/// and split into blocks with the given char separator.
/// </summary>
/// <param name="bytes">The bytes.</param>
/// <param name="separator">The separator.</param>
/// <param name="blockSize">The block size.</param>
/// <param name="blockCount">The block count.</param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Encode(byte[] bytes, char separator, int blockSize, int blockCount)
{
var length = bytes.Length;
var chars = new char[(length * 2) + blockCount];
var count = 0;
var size = 0;
var index = 0;
for (var i = 0; i < length; i++)
{
var byteIndex = bytes[i];
chars[index++] = HexLutHi[byteIndex];
chars[index++] = HexLutLo[byteIndex];
if (count == blockCount)
{
continue;
}
if (++size < blockSize)
{
continue;
}
chars[index++] = separator;
size = 0;
count++;
}
return new string(chars, 0, chars.Length);
}
}
}

View File

@@ -20,24 +20,11 @@ namespace Umbraco.Core.IO.MediaPathSchemes
{
// assumes that cuid and puid keys can be trusted - and that a single property type
// for a single content cannot store two different files with the same name
var directory = Combine(itemGuid, propertyGuid).ToHexString(/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/...
var directory = HexEncoder.Encode(GuidUtils.Combine(itemGuid, propertyGuid).ToByteArray()/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/...
return Path.Combine(directory, filename).Replace('\\', '/');
}
/// <inheritdoc />
public string GetDeleteDirectory(string filepath)
{
return Path.GetDirectoryName(filepath);
}
private static byte[] Combine(Guid guid1, Guid guid2)
{
var bytes1 = guid1.ToByteArray();
var bytes2 = guid2.ToByteArray();
var bytes = new byte[bytes1.Length];
for (var i = 0; i < bytes1.Length; i++)
bytes[i] = (byte) (bytes1[i] ^ bytes2[i]);
return bytes;
}
public string GetDeleteDirectory(string filepath) => Path.GetDirectoryName(filepath);
}
}

View File

@@ -111,7 +111,6 @@
<Compile Include="AttemptOfTResultTStatus.cs" />
<Compile Include="Components\AuditEventsComponent.cs" />
<Compile Include="BindingRedirects.cs" />
<Compile Include="ByteArrayExtensions.cs" />
<Compile Include="Cache\CacheHelper.cs" />
<Compile Include="Cache\CacheKeys.cs" />
<Compile Include="Cache\CacheProviderExtensions.cs" />
@@ -320,6 +319,8 @@
<Compile Include="Events\ExportedMemberEventArgs.cs" />
<Compile Include="Events\RolesEventArgs.cs" />
<Compile Include="Events\UserGroupWithUsers.cs" />
<Compile Include="GuidUtils.cs" />
<Compile Include="HexEncoder.cs" />
<Compile Include="IO\MediaPathSchemes\CombinedGuidsMediaPathScheme.cs" />
<Compile Include="IO\IMediaPathScheme.cs" />
<Compile Include="IO\MediaPathSchemes\OriginalMediaPathScheme.cs" />

View File

@@ -0,0 +1,48 @@
using System;
using BenchmarkDotNet.Attributes;
using Umbraco.Core;
using Umbraco.Tests.Benchmarks.Config;
namespace Umbraco.Tests.Benchmarks
{
[QuickRunWithMemoryDiagnoserConfig]
public class CombineGuidBenchmarks
{
private static readonly Guid _a = Guid.NewGuid();
private static readonly Guid _b = Guid.NewGuid();
[Benchmark]
public byte[] CombineUtils() => GuidUtils.Combine(_a, _b).ToByteArray();
[Benchmark]
public byte[] CombineLoop() => Combine(_a, _b);
private static byte[] Combine(Guid guid1, Guid guid2)
{
var bytes1 = guid1.ToByteArray();
var bytes2 = guid2.ToByteArray();
var bytes = new byte[bytes1.Length];
for (var i = 0; i < bytes1.Length; i++)
{
bytes[i] = (byte)(bytes1[i] ^ bytes2[i]);
}
return bytes;
}
}
// Nov 8 2018
//BenchmarkDotNet=v0.11.2, OS=Windows 10.0.17763.55 (1809/October2018Update/Redstone5)
//Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores
// [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0
// Job-JIATTD : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0
//IterationCount=3 IterationTime=100.0000 ms LaunchCount = 1
//WarmupCount=3
// Method | Mean | Error | StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
//------------- |---------:|----------:|----------:|------------:|------------:|------------:|--------------------:|
// CombineUtils | 33.34 ns | 8.086 ns | 0.4432 ns | 0.0133 | - | - | 28 B |
// CombineLoop | 55.03 ns | 11.311 ns | 0.6200 ns | 0.0395 | - | - | 84 B |
}

View File

@@ -0,0 +1,69 @@
using System;
using System.Text;
using BenchmarkDotNet.Attributes;
using Umbraco.Core;
using Umbraco.Tests.Benchmarks.Config;
namespace Umbraco.Tests.Benchmarks
{
[QuickRunConfig]
public class HexStringBenchmarks
{
private byte[] _buffer;
[Params(8, 16, 32, 64, 128, 256)]
public int Count { get; set; }
[GlobalSetup]
public void Setup()
{
this._buffer = new byte[this.Count];
var random = new Random();
random.NextBytes(this._buffer);
}
[Benchmark(Baseline = true)]
public string ToHexStringBuilder()
{
var sb = new StringBuilder(this._buffer.Length * 2);
for (var i = 0; i < this._buffer.Length; i++)
{
sb.Append(this._buffer[i].ToString("X2"));
}
return sb.ToString();
}
[Benchmark]
public string ToHexStringEncoder() => HexEncoder.Encode(this._buffer);
}
// Nov 8 2018
//BenchmarkDotNet=v0.11.2, OS=Windows 10.0.17763.55 (1809/October2018Update/Redstone5)
//Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores
// [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0
// Job-JIATTD : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0
//IterationCount=3 IterationTime=100.0000 ms LaunchCount = 1
//WarmupCount=3
// Method | Count | Mean | Error | StdDev | Ratio |
//------------------- |------ |-------------:|-------------:|-----------:|------:|
// ToHexStringBuilder | 8 | 786.49 ns | 319.92 ns | 17.536 ns | 1.00 |
// ToHexStringEncoder | 8 | 64.19 ns | 30.21 ns | 1.656 ns | 0.08 |
// | | | | | |
// ToHexStringBuilder | 16 | 1,442.43 ns | 503.00 ns | 27.571 ns | 1.00 |
// ToHexStringEncoder | 16 | 133.46 ns | 177.55 ns | 9.732 ns | 0.09 |
// | | | | | |
// ToHexStringBuilder | 32 | 2,869.23 ns | 924.35 ns | 50.667 ns | 1.00 |
// ToHexStringEncoder | 32 | 181.03 ns | 96.64 ns | 5.297 ns | 0.06 |
// | | | | | |
// ToHexStringBuilder | 64 | 5,775.33 ns | 2,825.42 ns | 154.871 ns | 1.00 |
// ToHexStringEncoder | 64 | 331.16 ns | 125.63 ns | 6.886 ns | 0.06 |
// | | | | | |
// ToHexStringBuilder | 128 | 11,662.35 ns | 4,908.03 ns | 269.026 ns | 1.00 |
// ToHexStringEncoder | 128 | 633.78 ns | 57.56 ns | 3.155 ns | 0.05 |
// | | | | | |
// ToHexStringBuilder | 256 | 22,960.11 ns | 14,111.47 ns | 773.497 ns | 1.00 |
// ToHexStringEncoder | 256 | 1,224.76 ns | 547.27 ns | 29.998 ns | 0.05 |
}

View File

@@ -46,10 +46,12 @@
</ItemGroup>
<ItemGroup>
<Compile Include="BulkInsertBenchmarks.cs" />
<Compile Include="CombineGuidBenchmarks.cs" />
<Compile Include="ConcurrentDictionaryBenchmarks.cs" />
<Compile Include="Config\QuickRunConfigAttribute.cs" />
<Compile Include="Config\QuickRunWithMemoryDiagnoserConfigAttribute.cs" />
<Compile Include="CtorInvokeBenchmarks.cs" />
<Compile Include="HexStringBenchmarks.cs" />
<Compile Include="LinqCastBenchmarks.cs" />
<Compile Include="ModelToSqlExpressionHelperBenchmarks.cs" />
<Compile Include="Program.cs" />
@@ -91,4 +93,4 @@
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
</Project>

View File

@@ -0,0 +1,32 @@
using System;
using NUnit.Framework;
using Umbraco.Core;
namespace Umbraco.Tests.CoreThings
{
public class GuidUtilsTests
{
[Test]
public void GuidCombineMethodsAreEqual()
{
var a = Guid.NewGuid();
var b = Guid.NewGuid();
Assert.AreEqual(GuidUtils.Combine(a, b).ToByteArray(), Combine(a, b));
}
// Reference implementation taken from original code.
private static byte[] Combine(Guid guid1, Guid guid2)
{
var bytes1 = guid1.ToByteArray();
var bytes2 = guid2.ToByteArray();
var bytes = new byte[bytes1.Length];
for (var i = 0; i < bytes1.Length; i++)
{
bytes[i] = (byte)(bytes1[i] ^ bytes2[i]);
}
return bytes;
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Text;
using NUnit.Framework;
using Umbraco.Core;
namespace Umbraco.Tests.CoreThings
{
public class HexEncoderTests
{
[Test]
public void ToHexStringCreatesCorrectValue()
{
var buffer = new byte[255];
var random = new Random();
random.NextBytes(buffer);
var sb = new StringBuilder(buffer.Length * 2);
for (var i = 0; i < buffer.Length; i++)
{
sb.Append(buffer[i].ToString("X2"));
}
var expected = sb.ToString();
var actual = HexEncoder.Encode(buffer);
Assert.AreEqual(expected, actual);
}
[Test]
public void ToHexStringWithSeparatorCreatesCorrectValue()
{
var buffer = new byte[255];
var random = new Random();
random.NextBytes(buffer);
var expected = ToHexString(buffer, '/', 2, 4);
var actual = HexEncoder.Encode(buffer, '/', 2, 4);
Assert.AreEqual(expected, actual);
}
private static readonly char[] _bytesToHexStringLookup = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
// Reference implementation taken from original extension method.
private static string ToHexString(byte[] bytes, char separator, int blockSize, int blockCount)
{
int p = 0, bytesLength = bytes.Length, count = 0, size = 0;
var chars = new char[(bytesLength * 2) + blockCount];
for (var i = 0; i < bytesLength; i++)
{
var b = bytes[i];
chars[p++] = _bytesToHexStringLookup[b / 0x10];
chars[p++] = _bytesToHexStringLookup[b % 0x10];
if (count == blockCount)
{
continue;
}
if (++size < blockSize)
{
continue;
}
chars[p++] = separator;
size = 0;
count++;
}
return new string(chars, 0, chars.Length);
}
}
}

View File

@@ -118,6 +118,8 @@
<Compile Include="Collections\OrderedHashSetTests.cs" />
<Compile Include="CoreThings\CallContextTests.cs" />
<Compile Include="Components\ComponentTests.cs" />
<Compile Include="CoreThings\GuidUtilsTests.cs" />
<Compile Include="CoreThings\HexEncoderTests.cs" />
<Compile Include="CoreXml\RenamedRootNavigatorTests.cs" />
<Compile Include="Manifest\ManifestContentAppTests.cs" />
<Compile Include="Migrations\MigrationPlanTests.cs" />