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