diff --git a/README.md b/README.md index c38d155..96ce89f 100644 --- a/README.md +++ b/README.md @@ -434,11 +434,11 @@ Benchmark scenarios also include comparisons against `Guid`, where functionality The following benchmarks were performed: ``` -BenchmarkDotNet v0.15.8, Windows 10 (10.0.19044.6809/21H2/November2021Update) +BenchmarkDotNet v0.15.8, Windows 10 (10.0.19044.7184/21H2/November2021Update) AMD Ryzen 7 3700X 3.60GHz, 1 CPU, 12 logical and 6 physical cores -.NET SDK 10.0.200 - [Host] : .NET 10.0.4 (10.0.4, 10.0.426.12010), X64 RyuJIT x86-64-v3 - DefaultJob : .NET 10.0.4 (10.0.4, 10.0.426.12010), X64 RyuJIT x86-64-v3 +.NET SDK 10.0.202 + [Host] : .NET 10.0.6 (10.0.6, 10.0.626.17701), X64 RyuJIT x86-64-v3 + DefaultJob : .NET 10.0.6 (10.0.6, 10.0.626.17701), X64 RyuJIT x86-64-v3 Job=DefaultJob @@ -465,10 +465,10 @@ Job=DefaultJob | FromByteArray | NUlid | 0.0325 ns | 0.0028 ns | - | - | | FromByteArray | Guid | 0.0232 ns | 0.0036 ns | - | - | -| FromGuid | ByteAetherUlid | 0.0230 ns | 0.0028 ns | - | - | -| FromGuid | NetUlid | 1.2526 ns | 0.0124 ns | - | - | -| FromGuid | Ulid | 1.7307 ns | 0.0146 ns | - | - | -| FromGuid | NUlid | 0.5142 ns | 0.0096 ns | - | - | +| FromGuid | ByteAetherUlid | 0.0000 ns | 0.0000 ns | - | - | +| FromGuid | NetUlid | 1.2034 ns | 0.0798 ns | - | - | +| FromGuid | Ulid | 1.4057 ns | 0.0437 ns | - | - | +| FromGuid | NUlid | 0.1920 ns | 0.0089 ns | - | - | | FromString | ByteAetherUlid | 13.6761 ns | 0.0780 ns | - | - | | FromString | NetUlid | 27.1021 ns | 0.2000 ns | - | - | @@ -482,10 +482,10 @@ Job=DefaultJob | ToByteArray | Ulid | 4.1402 ns | 0.1311 ns | 0.0048 | 40 B | | ToByteArray | NUlid | 4.5557 ns | 0.1312 ns | 0.0048 | 40 B | -| ToGuid | ByteAetherUlid | 0.0178 ns | 0.0045 ns | - | - | -| ToGuid | NetUlid | 10.4798 ns | 0.0226 ns | - | - | -| ToGuid | Ulid | 0.7470 ns | 0.0084 ns | - | - | -| ToGuid | NUlid | 0.1989 ns | 0.0028 ns | - | - | +| ToGuid | ByteAetherUlid | 0.0151 ns | 0.0031 ns | - | - | +| ToGuid | NetUlid | 9.9122 ns | 0.0858 ns | - | - | +| ToGuid | Ulid | 0.5244 ns | 0.0163 ns | - | - | +| ToGuid | NUlid | 0.1479 ns | 0.0042 ns | - | - | | ToString | ByteAetherUlid | 12.4213 ns | 0.2890 ns | 0.0096 | 80 B | | ToString | NetUlid | 24.3216 ns | 0.5226 ns | 0.0095 | 80 B | diff --git a/src/ByteAether.Ulid.Tests/Ulid.Boundaries.Tests.cs b/src/ByteAether.Ulid.Tests/Ulid.Boundaries.Tests.cs index a94d18a..f93ce6a 100644 --- a/src/ByteAether.Ulid.Tests/Ulid.Boundaries.Tests.cs +++ b/src/ByteAether.Ulid.Tests/Ulid.Boundaries.Tests.cs @@ -11,7 +11,7 @@ public void EmptyUlid_ShouldBeDefault() // Assert Assert.Equal(default, ulid); - Assert.Equal(emptyBytes, ulid.AsByteSpan()); + Assert.Equal(emptyBytes, ulid.ToByteArray()); } [Fact] @@ -22,7 +22,7 @@ public void MaxUlid_ShouldHaveAllBytesSetToMax() var expected = Enumerable.Repeat((byte)0xFF, 16).ToArray(); // Assert - Assert.Equal(expected, ulid.AsByteSpan()); + Assert.Equal(expected, ulid.ToByteArray()); } [Fact] @@ -36,7 +36,7 @@ public void MinAt_WithLongTimestamp_ShouldHaveZeroRandomComponent() // Assert // Last 10 bytes (random part) should be 0 - Assert.All(ulid.AsByteSpan()[^10..].ToArray(), x => Assert.Equal(0, x)); + Assert.All(ulid.AsByteSpan().Slice(6, 10).ToArray(), x => Assert.Equal(0, x)); Assert.Equal(timestamp, ulid.Time.ToUnixTimeMilliseconds()); } @@ -51,7 +51,7 @@ public void MinAt_WithDateTimeOffset_ShouldHaveZeroRandomComponent() // Assert // Last 10 bytes (random part) should be 0 - Assert.All(ulid.AsByteSpan()[^10..].ToArray(), x => Assert.Equal(0, x)); + Assert.All(ulid.AsByteSpan().Slice(6, 10).ToArray(), x => Assert.Equal(0, x)); Assert.Equal(dto.ToUnixTimeMilliseconds(), ulid.Time.ToUnixTimeMilliseconds()); } @@ -66,7 +66,7 @@ public void MaxAt_WithLongTimestamp_ShouldHaveMaxRandomComponent() // Assert // Last 10 bytes (random part) should be 0xFF - Assert.All(ulid.AsByteSpan()[^10..].ToArray(), x => Assert.Equal(0xFF, x)); + Assert.All(ulid.AsByteSpan().Slice(6, 10).ToArray(), x => Assert.Equal(0xFF, x)); Assert.Equal(timestamp, ulid.Time.ToUnixTimeMilliseconds()); } @@ -81,7 +81,7 @@ public void MaxAt_WithDateTimeOffset_ShouldHaveMaxRandomComponent() // Assert // Last 10 bytes (random part) should be 0xFF - Assert.All(ulid.AsByteSpan()[^10..].ToArray(), x => Assert.Equal(0xFF, x)); + Assert.All(ulid.AsByteSpan().Slice(6, 10).ToArray(), x => Assert.Equal(0xFF, x)); Assert.Equal(dto.ToUnixTimeMilliseconds(), ulid.Time.ToUnixTimeMilliseconds()); } } \ No newline at end of file diff --git a/src/ByteAether.Ulid/Ulid.Guid.cs b/src/ByteAether.Ulid/Ulid.Guid.cs index 419ca58..8e21318 100644 --- a/src/ByteAether.Ulid/Ulid.Guid.cs +++ b/src/ByteAether.Ulid/Ulid.Guid.cs @@ -20,52 +20,22 @@ public readonly partial struct Ulid /// Creates a new ULID using the specified GUID. /// /// The GUID to initialize the ULID with. - // HACK: We assume the layout of a Guid is the following: - // Int32, Int16, Int16, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8 - // source: https://github.com/dotnet/runtime/blob/5c4686f831d34c2c127e943d0f0d144793eeb0ad/src/libraries/System.Private.CoreLib/src/System/Guid.cs #if NET5_0_OR_GREATER [SkipLocalsInit] #endif #if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveOptimization)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Ulid New(Guid guid) { -#if NET6_0_OR_GREATER - if (BitConverter.IsLittleEndian && _isVector128Supported) - { - var vector = Unsafe.As>(ref guid); - var shuffled = Shuffle(vector); - - return Unsafe.As, Ulid>(ref shuffled); - } -#endif - Span ulidBytes = stackalloc byte[_ulidSize]; - if (BitConverter.IsLittleEndian) { - // |A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P| - // |D|C|B|A|... - // ...|F|E|H|G|... - // ...|I|J|K|L|M|N|O|P| - ref var ptr = ref Unsafe.As(ref guid); - var lower = BinaryPrimitives.ReverseEndianness(ptr); - MemoryMarshal.Write(ulidBytes, ref lower); - - - ptr = ref Unsafe.Add(ref ptr, 1); - var upper = ((ptr & 0x00_FF_00_FF) << 8) | ((ptr & 0xFF_00_FF_00) >> 8); - MemoryMarshal.Write(ulidBytes[4..], ref upper); - - ref var upperBytes = ref Unsafe.As(ref Unsafe.Add(ref ptr, 1)); - MemoryMarshal.Write(ulidBytes[8..], ref upperBytes); - } - else - { - MemoryMarshal.Write(ulidBytes, ref guid); + return Shuffle(ref guid); } - return MemoryMarshal.Read(ulidBytes); + return Unsafe.As(ref guid); } /// @@ -76,43 +46,18 @@ public static Ulid New(Guid guid) [SkipLocalsInit] #endif #if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveOptimization)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public Guid ToGuid() { -#if NETCOREAPP - if (BitConverter.IsLittleEndian && _isVector128Supported) - { - var vector = Unsafe.As>(ref Unsafe.AsRef(in this)); - var shuffled = Shuffle(vector); - - return Unsafe.As, Guid>(ref shuffled); - } -#endif - Span guidBytes = stackalloc byte[_ulidSize]; if (BitConverter.IsLittleEndian) { - // |A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P| - // |D|C|B|A|... - // ...|F|E|H|G|... - // ...|I|J|K|L|M|N|O|P| - ref var ptr = ref Unsafe.As(ref Unsafe.AsRef(in this)); - var lower = BinaryPrimitives.ReverseEndianness(ptr); - MemoryMarshal.Write(guidBytes, ref lower); - - ptr = ref Unsafe.Add(ref ptr, 1); - var upper = ((ptr & 0x00_FF_00_FF) << 8) | ((ptr & 0xFF_00_FF_00) >> 8); - MemoryMarshal.Write(guidBytes[4..], ref upper); - - ref var upperBytes = ref Unsafe.As(ref Unsafe.Add(ref ptr, 1)); - MemoryMarshal.Write(guidBytes[8..], ref upperBytes); - } - else - { - MemoryMarshal.Write(guidBytes, ref Unsafe.AsRef(in this)); + return Shuffle(ref Unsafe.AsRef(in this)); } - return MemoryMarshal.Read(guidBytes); + return Unsafe.As(ref Unsafe.AsRef(in this)); } /// @@ -140,29 +85,66 @@ public Guid ToGuid() public static implicit operator Ulid(Guid guid) => New(guid); #if NETCOREAPP - private static readonly bool _isVector128Supported = + private static readonly Vector128 _shuffleMask + = Vector128.Create((byte)3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15); + + private static readonly bool _isAccelerated = #if NET7_0_OR_GREATER - Vector128.IsHardwareAccelerated; -#else + Vector128.IsHardwareAccelerated || +#endif Ssse3.IsSupported; #endif - private static readonly Vector128 _shuffleMask - = Vector128.Create((byte)3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15); - + // HACK: We assume the layout of a Guid is the following: + // Int32, Int16, Int16, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8 + // Source: https://github.com/dotnet/runtime/blob/5c4686f831d34c2c127e943d0f0d144793eeb0ad/src/libraries/System.Private.CoreLib/src/System/Guid.cs + // More info: https://stackoverflow.com/questions/10190817/guid-byte-order-in-net/10191075#10191075 +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif #if NETCOREAPP3_0_OR_GREATER [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] #else [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static Vector128 Shuffle(Vector128 value) + private static TOut Shuffle(ref TIn bytes) { - return +#if NETCOREAPP + if (_isAccelerated) + { + var vector = Unsafe.As>(ref bytes); + #if NET7_0_OR_GREATER - Vector128.IsHardwareAccelerated ? Vector128.Shuffle(value, _shuffleMask) : + if (Vector128.IsHardwareAccelerated) + { + vector = Vector128.Shuffle(vector, _shuffleMask); + return Unsafe.As, TOut>(ref vector); + } #endif - Ssse3.IsSupported ? Ssse3.Shuffle(value, _shuffleMask) : - throw new NotImplementedException(); - } + + vector = Ssse3.Shuffle(vector, _shuffleMask); + return Unsafe.As, TOut>(ref vector); + } #endif + + // |A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P| + // |D|C|B|A|... + // ...|F|E|H|G|... + // ...|I|J|K|L|M|N|O|P| + Span result = new byte[_ulidSize]; + + ref var ptr = ref Unsafe.As(ref bytes); + var lower = BinaryPrimitives.ReverseEndianness(ptr); + + ptr = ref Unsafe.Add(ref ptr, 1); + var upper = ((ptr & 0x00_FF_00_FF) << 8) | ((ptr & 0xFF_00_FF_00) >> 8); + + ref var upperBytes = ref Unsafe.As(ref Unsafe.Add(ref ptr, 1)); + + MemoryMarshal.Write(result, ref lower); + MemoryMarshal.Write(result[4..], ref upper); + MemoryMarshal.Write(result[8..], ref upperBytes); + + return Unsafe.As(ref result.GetPinnableReference()); + } } \ No newline at end of file