diff --git a/README.md b/README.md index c822a2e..6614a76 100644 --- a/README.md +++ b/README.md @@ -439,7 +439,7 @@ Benchmark scenarios also include comparisons against `Guid`, where functionality The following benchmarks were performed: ``` BenchmarkDotNet v0.15.8, Windows 10 (10.0.19044.7417/21H2/November2021Update) -AMD Ryzen 7 3700X 3.60GHz, 1 CPU, 12 logical and 6 physical cores +AMD Ryzen 7 3700X 3.60GHz, 1 CPU, 8 logical and 4 physical cores .NET SDK 10.0.301 [Host] : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3 DefaultJob : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3 @@ -448,72 +448,72 @@ Job=DefaultJob | Type | Method | Mean | Error | Gen0 | Allocated | |---------------- |------------------- |------------:|----------:|-------:|----------:| -| Generate | ByteAetherUlid | 41.0279 ns | 0.0968 ns | - | - | -| Generate | ByteAetherUlidR1Bp | 47.2891 ns | 0.1171 ns | - | - | -| Generate | ByteAetherUlidR4Bp | 51.9968 ns | 0.0910 ns | - | - | -| Generate | ByteAetherUlidR1Bc | 90.0880 ns | 0.1864 ns | - | - | -| Generate | ByteAetherUlidR4Bc | 96.0395 ns | 0.2255 ns | - | - | -| Generate | NetUlid *(1) | 158.6095 ns | 0.9265 ns | 0.0095 | 80 B | -| Generate | NUlid *(2) | 48.7143 ns | 0.0948 ns | - | - | - -| GenerateNonMono | ByteAetherUlid | 90.1055 ns | 0.3176 ns | - | - | -| GenerateNonMono | ByteAetherUlidP | 41.1849 ns | 0.0736 ns | - | - | -| GenerateNonMono | Ulid *(3,4) | 39.2001 ns | 0.1008 ns | - | - | -| GenerateNonMono | NUlid | 93.0571 ns | 0.1699 ns | - | - | -| GenerateNonMono | Guid *(5) | 47.1776 ns | 0.0932 ns | - | - | -| GenerateNonMono | GuidV7 *(3,5) | 77.4890 ns | 0.2267 ns | - | - | - -| FromByteArray | ByteAetherUlid | 0.8044 ns | 0.0070 ns | - | - | -| FromByteArray | NetUlid | 1.4115 ns | 0.0111 ns | - | - | -| FromByteArray | Ulid | 1.1808 ns | 0.0080 ns | - | - | -| FromByteArray | NUlid | 1.1515 ns | 0.0063 ns | - | - | -| FromByteArray | Guid | 1.0440 ns | 0.0059 ns | - | - | - -| FromGuid | ByteAetherUlid | 0.8255 ns | 0.0057 ns | - | - | -| FromGuid | NetUlid | 2.0028 ns | 0.0373 ns | - | - | -| FromGuid | Ulid | 1.9495 ns | 0.0116 ns | - | - | -| FromGuid | NUlid | 0.9240 ns | 0.0183 ns | - | - | - -| FromString | ByteAetherUlid | 14.4357 ns | 0.0392 ns | - | - | -| FromString | NetUlid | 27.2159 ns | 0.0690 ns | - | - | -| FromString | Ulid | 16.9972 ns | 0.0311 ns | - | - | -| FromString | NUlid | 53.9897 ns | 0.1649 ns | 0.0086 | 72 B | -| FromString | Guid | 21.8639 ns | 0.1228 ns | - | - | - -| ToByteArray | ByteAetherUlid | 4.7470 ns | 0.1274 ns | 0.0048 | 40 B | -| ToByteArray | AsByteSpan *(6) | 0.7736 ns | 0.0051 ns | - | - | -| ToByteArray | NetUlid | 9.4871 ns | 0.1054 ns | 0.0048 | 40 B | -| ToByteArray | Ulid | 4.7189 ns | 0.1066 ns | 0.0048 | 40 B | -| ToByteArray | NUlid | 8.7597 ns | 0.1460 ns | 0.0048 | 40 B | - -| ToGuid | ByteAetherUlid | 0.8170 ns | 0.0070 ns | - | - | -| ToGuid | NetUlid | 10.3155 ns | 0.0230 ns | - | - | -| ToGuid | Ulid | 0.9731 ns | 0.0074 ns | - | - | -| ToGuid | NUlid | 0.7636 ns | 0.0061 ns | - | - | - -| ToString | ByteAetherUlid | 21.5166 ns | 0.3377 ns | 0.0095 | 80 B | -| ToString | NetUlid | 27.3102 ns | 0.3004 ns | 0.0095 | 80 B | -| ToString | Ulid | 23.4614 ns | 0.2211 ns | 0.0095 | 80 B | -| ToString | NUlid | 29.4123 ns | 0.2632 ns | 0.0095 | 80 B | -| ToString | Guid | 10.2546 ns | 0.2493 ns | 0.0115 | 96 B | - -| CompareTo | ByteAetherUlid | 1.4082 ns | 0.0071 ns | - | - | -| CompareTo | NetUlid | 4.4499 ns | 0.0303 ns | - | - | -| CompareTo | Ulid | 6.6206 ns | 0.0352 ns | - | - | -| CompareTo | NUlid | 9.2860 ns | 0.0495 ns | - | - | -| CompareTo | Guid | 4.8326 ns | 0.0210 ns | - | - | - -| Equals | ByteAetherUlid | 1.0720 ns | 0.0135 ns | - | - | -| Equals | NetUlid | 1.9610 ns | 0.0155 ns | - | - | -| Equals | Ulid | 1.0432 ns | 0.0052 ns | - | - | -| Equals | NUlid | 1.0552 ns | 0.0103 ns | - | - | -| Equals | Guid | 1.1030 ns | 0.0107 ns | - | - | - -| GetHashCode | ByteAetherUlid | 0.9185 ns | 0.0053 ns | - | - | -| GetHashCode | NetUlid | 8.8601 ns | 0.0250 ns | - | - | -| GetHashCode | Ulid | 0.9362 ns | 0.0059 ns | - | - | -| GetHashCode | NUlid | 6.9396 ns | 0.0387 ns | - | - | -| GetHashCode | Guid | 0.9082 ns | 0.0049 ns | - | - | +| Generate | ByteAetherUlid | 40.5719 ns | 0.6657 ns | - | - | +| Generate | ByteAetherUlidR1Bp | 48.3766 ns | 0.1168 ns | - | - | +| Generate | ByteAetherUlidR4Bp | 49.9384 ns | 0.0914 ns | - | - | +| Generate | ByteAetherUlidR1Bc | 89.6066 ns | 0.6575 ns | - | - | +| Generate | ByteAetherUlidR4Bc | 91.5831 ns | 0.3683 ns | - | - | +| Generate | NetUlid *(1) | 162.1996 ns | 0.7979 ns | 0.0095 | 80 B | +| Generate | NUlid *(2) | 49.6330 ns | 0.1247 ns | - | - | + +| GenerateNonMono | ByteAetherUlid | 89.6904 ns | 0.5098 ns | - | - | +| GenerateNonMono | ByteAetherUlidP | 40.9386 ns | 0.1507 ns | - | - | +| GenerateNonMono | Ulid *(3,4) | 39.5954 ns | 0.0592 ns | - | - | +| GenerateNonMono | NUlid | 93.1371 ns | 0.4716 ns | - | - | +| GenerateNonMono | Guid *(5) | 47.6369 ns | 0.1340 ns | - | - | +| GenerateNonMono | GuidV7 *(3,5) | 78.6568 ns | 0.4788 ns | - | - | + +| FromByteArray | ByteAetherUlid | 0.7974 ns | 0.0076 ns | - | - | +| FromByteArray | NetUlid | 1.4321 ns | 0.0109 ns | - | - | +| FromByteArray | Ulid | 1.1894 ns | 0.0115 ns | - | - | +| FromByteArray | NUlid | 1.1603 ns | 0.0105 ns | - | - | +| FromByteArray | Guid | 1.0478 ns | 0.0084 ns | - | - | + +| FromGuid | ByteAetherUlid | 0.8286 ns | 0.0052 ns | - | - | +| FromGuid | NetUlid | 2.5073 ns | 0.1470 ns | - | - | +| FromGuid | Ulid | 2.3504 ns | 0.0776 ns | - | - | +| FromGuid | NUlid | 0.9781 ns | 0.0437 ns | - | - | + +| FromString | ByteAetherUlid | 15.2724 ns | 0.2616 ns | - | - | +| FromString | NetUlid | 28.4821 ns | 0.5618 ns | - | - | +| FromString | Ulid | 17.8202 ns | 0.2157 ns | - | - | +| FromString | NUlid | 64.0466 ns | 1.4103 ns | 0.0086 | 72 B | +| FromString | Guid | 22.5952 ns | 0.4522 ns | - | - | + +| ToByteArray | ByteAetherUlid | 4.6165 ns | 0.1121 ns | 0.0048 | 40 B | +| ToByteArray | AsByteSpan *(6) | 0.7794 ns | 0.0064 ns | - | - | +| ToByteArray | NetUlid | 9.4252 ns | 0.0894 ns | 0.0048 | 40 B | +| ToByteArray | Ulid | 4.5432 ns | 0.0997 ns | 0.0048 | 40 B | +| ToByteArray | NUlid | 8.5760 ns | 0.1168 ns | 0.0048 | 40 B | + +| ToGuid | ByteAetherUlid | 0.8083 ns | 0.0102 ns | - | - | +| ToGuid | NetUlid | 10.4080 ns | 0.0285 ns | - | - | +| ToGuid | Ulid | 1.2320 ns | 0.0102 ns | - | - | +| ToGuid | NUlid | 0.7692 ns | 0.0057 ns | - | - | + +| ToString | ByteAetherUlid | 19.1190 ns | 0.2176 ns | 0.0095 | 80 B | +| ToString | NetUlid | 23.7245 ns | 0.1987 ns | 0.0095 | 80 B | +| ToString | Ulid | 19.8952 ns | 0.2056 ns | 0.0095 | 80 B | +| ToString | NUlid | 29.1001 ns | 0.1223 ns | 0.0095 | 80 B | +| ToString | Guid | 7.9173 ns | 0.0257 ns | 0.0115 | 96 B | + +| CompareTo | ByteAetherUlid | 1.3642 ns | 0.0089 ns | - | - | +| CompareTo | NetUlid | 4.7076 ns | 0.0190 ns | - | - | +| CompareTo | Ulid | 6.7843 ns | 0.0188 ns | - | - | +| CompareTo | NUlid | 9.1551 ns | 0.0284 ns | - | - | +| CompareTo | Guid | 4.7391 ns | 0.0188 ns | - | - | + +| Equals | ByteAetherUlid | 1.1228 ns | 0.0164 ns | - | - | +| Equals | NetUlid | 1.9661 ns | 0.0091 ns | - | - | +| Equals | Ulid | 1.1092 ns | 0.0156 ns | - | - | +| Equals | NUlid | 1.0888 ns | 0.0063 ns | - | - | +| Equals | Guid | 1.0827 ns | 0.0085 ns | - | - | + +| GetHashCode | ByteAetherUlid | 0.9166 ns | 0.0046 ns | - | - | +| GetHashCode | NetUlid | 8.9307 ns | 0.0230 ns | - | - | +| GetHashCode | Ulid | 0.9288 ns | 0.0083 ns | - | - | +| GetHashCode | NUlid | 7.0085 ns | 0.0312 ns | - | - | +| GetHashCode | Guid | 0.9337 ns | 0.0068 ns | - | - | ``` Existing competitive libraries exhibit various deviations from the official ULID specification or present drawbacks: diff --git a/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs b/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs index 96c8417..3b7c090 100644 --- a/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs +++ b/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs @@ -175,7 +175,7 @@ int incrementSize initialRandom[9] = 0xA; // 10 var increment = new byte[incrementSize]; - increment[incrementSize - 1] = 4; // 10 + 4 + increment[0] = 4; // 10 + 4 var incrementedRandom = new byte[10]; incrementedRandom[9] = 15; // 10 + 4 + 1 : +1 comes from base implementation of Ulid @@ -221,7 +221,7 @@ public void New_MonotonicRandom_ShouldCarryOverIncrement( initialRandom[9] = 0xFE; // Max - 2 var increment = new byte[incrementSize]; - increment[incrementSize - 1] = 0x01; // 1 as the other +1 comes from base implementation + increment[0] = 0x01; // 1 as the other +1 comes from base implementation var initialRandomProvider = new ControllableRandomProvider(initialRandom); var incrementRandomProvider = new ControllableRandomProvider(increment); diff --git a/src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs b/src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs index 8e7463b..898979b 100644 --- a/src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs +++ b/src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs @@ -5,30 +5,11 @@ #pragma warning disable // https://github.com/dotnet/runtime/blob/8d796d8e60a5236cbd5f113ead1d3831064cdba1/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/MemoryMarshal.cs#L226 -// ref T GetArrayDataReference(T[] array) is original. namespace ByteAether.Ulid.Compatibility; public static class MemoryMarshal { - /// - /// Returns a reference to the 0th element of . If the array is empty, returns a reference to where the 0th element - /// would have been stored. Such a reference may be used for pinning but must never be dereferenced. - /// - /// is . - /// - /// This method does not perform array variance checks. The caller must manually perform any array variance checks - /// if the caller wishes to write to the returned reference. - /// - public static unsafe ref T GetArrayDataReference(T[] array) - where T : unmanaged - { - fixed (T* ptr = array) - { - return ref Unsafe.AsRef(ptr); - } - } - /// /// Creates a new span over a portion of a regular managed object. This can be useful /// if part of a managed object represents a "fixed array." This is dangerous because the @@ -45,22 +26,5 @@ public static unsafe ref T GetArrayDataReference(T[] array) [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe static Span CreateSpan(scoped ref T reference, int length) => new Span(Unsafe.AsPointer(ref reference), length); - - /// - /// Creates a new read-only span over a portion of a regular managed object. This can be useful - /// if part of a managed object represents a "fixed array." This is dangerous because the - /// is not checked. - /// - /// A reference to data. - /// The number of elements the memory contains. - /// A read-only span representing the specified reference and length. - /// - /// This method should be used with caution. It is dangerous because the length argument is not checked. - /// Even though the ref is annotated as scoped, it will be stored into the returned span, and the lifetime - /// of the returned span will not be validated for safety, even by span-aware languages. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe static ReadOnlySpan CreateReadOnlySpan(scoped ref T reference, int length) => - new ReadOnlySpan(Unsafe.AsPointer(ref reference), length); } #endif \ No newline at end of file diff --git a/src/ByteAether.Ulid/LowLatencyLock.cs b/src/ByteAether.Ulid/LowLatencyLock.cs new file mode 100644 index 0000000..41f5aa1 --- /dev/null +++ b/src/ByteAether.Ulid/LowLatencyLock.cs @@ -0,0 +1,123 @@ +using System.Runtime.CompilerServices; +#if !NET7_0_OR_GREATER +using System.Runtime.InteropServices; +#endif + +namespace ByteAether.Ulid; + +internal struct LowLatencyLock +{ + internal int LockState; + + internal readonly ref struct Scope + { +#if NET7_0_OR_GREATER + private readonly ref int _lockState; + + [SkipLocalsInit] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal Scope(ref LowLatencyLock @lock) + { + _lockState = ref @lock.LockState; + } + + [SkipLocalsInit] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void Dispose() + { + Volatile.Write(ref _lockState, 0); + } +#else + // This is fully supported in .NET Standard 2.0 / .NET 5.0. + private readonly Span _lockState; + +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + internal Scope(ref LowLatencyLock @lock) + { + _lockState = +#if NETCOREAPP + MemoryMarshal.CreateSpan(ref @lock.LockState, 1); +#else + Compatibility.MemoryMarshal.CreateSpan(ref @lock.LockState, 1); +#endif + ; + } + +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public void Dispose() + { + Volatile.Write(ref MemoryMarshal.GetReference(_lockState), 0); + } +#endif + + } +} + +internal static class LowLatencyLockExtensions +{ + private const int _spinWaitAttemptCount = 8; + private const int _spinWaitIterationCount = 8; + +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static LowLatencyLock.Scope Enter(ref this LowLatencyLock @lock) + { + if (Interlocked.CompareExchange(ref @lock.LockState, 1, 0) != 0) + { + ContendedEnter(ref @lock); + } + + return new(ref @lock); + } + +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + private static void ContendedEnter(ref LowLatencyLock @lock) + { + // First we do Thread.SpinWait() for limited amount of times + var remainingAttempts = _spinWaitAttemptCount; + do + { + Thread.SpinWait(_spinWaitIterationCount); + + // Test-and-Test-and-Set (TTAS) + // Initial read-only check keeps the cache line in the "Shared" state + if (@lock.LockState == 0 && Interlocked.CompareExchange(ref @lock.LockState, 1, 0) == 0) + { + return; + } + } while (--remainingAttempts > 0); + + // Later we do Thread.Yield() + do + { + Thread.Yield(); + } while (@lock.LockState != 0 || Interlocked.CompareExchange(ref @lock.LockState, 1, 0) != 0); + } +} \ No newline at end of file diff --git a/src/ByteAether.Ulid/Ulid.Comparable.cs b/src/ByteAether.Ulid/Ulid.Comparable.cs index 9df8c95..12d32cd 100644 --- a/src/ByteAether.Ulid/Ulid.Comparable.cs +++ b/src/ByteAether.Ulid/Ulid.Comparable.cs @@ -1,5 +1,4 @@ -using System.Buffers.Binary; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; #if NET7_0_OR_GREATER using System.Numerics; #endif @@ -104,16 +103,16 @@ private static int CompareToCore(in Ulid left, in Ulid right) ref var rA = ref Unsafe.As(ref Unsafe.AsRef(in left)); ref var rB = ref Unsafe.As(ref Unsafe.AsRef(in right)); - var a = BinaryPrimitives.ReverseEndianness(rA); - var b = BinaryPrimitives.ReverseEndianness(rB); + var a = ReverseOnLittleEndian(rA); + var b = ReverseOnLittleEndian(rB); if (a != b) { return a < b ? -1 : 1; } - a = BinaryPrimitives.ReverseEndianness(Unsafe.Add(ref rA, 1)); - b = BinaryPrimitives.ReverseEndianness(Unsafe.Add(ref rB, 1)); + a = ReverseOnLittleEndian(Unsafe.Add(ref rA, 1)); + b = ReverseOnLittleEndian(Unsafe.Add(ref rB, 1)); if (a != b) { diff --git a/src/ByteAether.Ulid/Ulid.Guid.cs b/src/ByteAether.Ulid/Ulid.Guid.cs index 028ecec..c51e3a1 100644 --- a/src/ByteAether.Ulid/Ulid.Guid.cs +++ b/src/ByteAether.Ulid/Ulid.Guid.cs @@ -3,8 +3,10 @@ using System.Runtime.InteropServices; #if NETCOREAPP using System.Runtime.Intrinsics; +#if !NET7_0_OR_GREATER using System.Runtime.Intrinsics.X86; #endif +#endif namespace ByteAether.Ulid; diff --git a/src/ByteAether.Ulid/Ulid.New.cs b/src/ByteAether.Ulid/Ulid.New.cs index 825cf6f..d924832 100644 --- a/src/ByteAether.Ulid/Ulid.New.cs +++ b/src/ByteAether.Ulid/Ulid.New.cs @@ -17,8 +17,6 @@ public readonly partial struct Ulid /// public static GenerationOptions DefaultGenerationOptions { get; set; } = new(); - private static readonly byte[] _lastUlid = new byte[_ulidSize]; - // Constant for Unix Epoch (1970-01-01 UTC) in Ticks private const long _unixEpochTicks = 621355968000000000; @@ -111,14 +109,9 @@ public static Ulid New(long timestamp, GenerationOptions? options = null) ref var ulidRef = ref Unsafe.As(ref ulid); // Fill timestamp - BinaryPrimitives.WriteUInt64BigEndian( -#if NETCOREAPP - MemoryMarshal.CreateSpan(ref ulidRef, 8), -#else - Compatibility.MemoryMarshal.CreateSpan(ref ulidRef, 8), -#endif - (ulong)timestamp << 16 - ); + var ts = (ulong)timestamp << 16; + ts = ReverseOnLittleEndian(ts); + Unsafe.WriteUnaligned(ref ulidRef, ts); FillRandom(ref ulidRef, timestamp, options ?? DefaultGenerationOptions); @@ -154,15 +147,11 @@ public static Ulid New(long timestamp, Span random) ref var ulidRef = ref Unsafe.As(ref ulid); // Fill timestamp - BinaryPrimitives.WriteUInt64BigEndian( -#if NETCOREAPP - MemoryMarshal.CreateSpan(ref ulidRef, 8), -#else - Compatibility.MemoryMarshal.CreateSpan(ref ulidRef, 8), -#endif - (ulong)timestamp << 16 - ); + var ts = (ulong)timestamp << 16; + ts = ReverseOnLittleEndian(ts); + Unsafe.WriteUnaligned(ref ulidRef, ts); + // Fill random Unsafe.CopyBlockUnaligned( ref Unsafe.Add(ref ulidRef, _ulidSizeTime), ref random.GetPinnableReference(), @@ -172,32 +161,23 @@ ref random.GetPinnableReference(), return ulid; } - private static int _lastUlidLock; - -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - private static void AcquireSpinLock() + // Separates Lock and LastUlid into different cache lines to prevent "false sharing" + // x64 has 64-byte cache lines; ARM64 (e.g., Apple Silicon) has 128-byte cache lines + [StructLayout(LayoutKind.Explicit)] + private class State { - // Hot-path - if (Interlocked.CompareExchange(ref _lastUlidLock, 1, 0) == 0) - { - return; - } - - // Spin until the lock is acquired - var spinner = new SpinWait(); - while (Interlocked.CompareExchange(ref _lastUlidLock, 1, 0) == 1) - { - spinner.SpinOnce(); - } + // Cache Line 1 + [FieldOffset(16)] public LowLatencyLock Lock; + + // Cache Line 2(ARM64)/3(x64) +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + [FieldOffset(16+128)] public ulong LastUlidPart0; + [FieldOffset(16+128+8)] public ulong LastUlidPart1; +#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value } + private static readonly State _state = new(); + #if NET5_0_OR_GREATER [SkipLocalsInit] #endif @@ -207,85 +187,78 @@ private static void AcquireSpinLock() private static void FillRandom(ref byte ulidBytesRef, long timestamp, GenerationOptions options) { // Calculate offset to a random part - ref var randomPartRef = ref Unsafe.Add(ref ulidBytesRef, _ulidSizeTime); + ref var ulidBytesRandomRef = ref Unsafe.Add(ref ulidBytesRef, _ulidSizeTime); + var monotonicity = options.Monotonicity; - if (options.Monotonicity == GenerationOptions.MonotonicityOptions.NonMonotonic) + if (monotonicity == GenerationOptions.MonotonicityOptions.NonMonotonic) { options.InitialRandomSource.GetBytes( #if NETCOREAPP - MemoryMarshal.CreateSpan(ref randomPartRef, _ulidSizeRandom) + MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) #else - Compatibility.MemoryMarshal.CreateSpan(ref randomPartRef, _ulidSizeRandom) + Compatibility.MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) #endif ); return; } - ref var lastUlidRef = -#if NETCOREAPP - ref MemoryMarshal.GetArrayDataReference(_lastUlid); -#else - ref Compatibility.MemoryMarshal.GetArrayDataReference(_lastUlid); -#endif - - ref var lastRandomRef = ref Unsafe.Add(ref lastUlidRef, _ulidSizeTime); + ref var lastUlidRef = ref Unsafe.As(ref _state.LastUlidPart0); - AcquireSpinLock(); - try + using(_state.Lock.Enter()) { - // Read the last timestamp raw (from bytes 0-7 of lastUlid) + // Read the last timestamp (from bytes 0-7 of "last ULID") // Shift it to get 48 bits. - var lastTime = (long)( - BinaryPrimitives.ReadUInt64BigEndian( + var lastTime = ReverseOnLittleEndian(_state.LastUlidPart0); + lastTime >>= 16; + + // If the timestamp is bigger than the last one, generate a new ULID + if (timestamp > (long)lastTime) + { + // We work on "generated ULID", then copy it into "last ULID" + + // Generate a new random to the generated ULID + options.InitialRandomSource.GetBytes( #if NETCOREAPP - MemoryMarshal.CreateReadOnlySpan(ref lastUlidRef, sizeof(ulong)) + MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) #else - Compatibility.MemoryMarshal.CreateReadOnlySpan(ref lastUlidRef, sizeof(ulong)) + Compatibility.MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, _ulidSizeRandom) #endif - ) >> 16 - ); + ); - // If the timestamp is the same or lesser than the last one, increment the last ULID by one - if (timestamp <= lastTime) + // Copy full generated ULID back to last ULID + Unsafe.CopyBlock(ref lastUlidRef, ref ulidBytesRef, _ulidSize); + } + else // Otherwise, increment the last ULID { - if (options.Monotonicity == GenerationOptions.MonotonicityOptions.MonotonicIncrement) + // We work on "last ULID", then copy it into "generated ULID" + + if (monotonicity == GenerationOptions.MonotonicityOptions.MonotonicIncrement) { - IncrementByOne(ref lastUlidRef); + LastUlidIncrement(0); } else { // We can use the random bytes of incomplete ULID for the random increment span var tempSpan = #if NETCOREAPP - MemoryMarshal.CreateSpan(ref randomPartRef, (int)options.Monotonicity); + MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, sizeof(uint)); #else - Compatibility.MemoryMarshal.CreateSpan(ref randomPartRef, (int)options.Monotonicity); + Compatibility.MemoryMarshal.CreateSpan(ref ulidBytesRandomRef, sizeof(uint)); #endif - options.IncrementRandomSource.GetBytes(tempSpan); - IncrementByByteSpan(ref lastUlidRef, tempSpan); + options.IncrementRandomSource.GetBytes(tempSpan[..(int)monotonicity]); + var increment = BinaryPrimitives.ReadUInt32LittleEndian(tempSpan); + + // The tempSpan may contain garbage, so mask that out + var totalBitsToKeep = (int)monotonicity * 8; + var mask = (uint)((1UL << totalBitsToKeep) - 1); + increment &= mask; + + LastUlidIncrement(increment); } - } - else // Otherwise, generate a new ULID - { - // Copy timestamp from the incomplete ULID - Unsafe.CopyBlock(ref lastUlidRef, ref ulidBytesRef, _ulidSizeTime); - // Generate a new random to the last ULID - options.InitialRandomSource.GetBytes( -#if NETCOREAPP - MemoryMarshal.CreateSpan(ref lastRandomRef, _ulidSizeRandom) -#else - Compatibility.MemoryMarshal.CreateSpan(ref lastRandomRef, _ulidSizeRandom) -#endif - ); + // Copy full last ULID back to generated ULID + Unsafe.CopyBlock(ref ulidBytesRef, ref lastUlidRef, _ulidSize); } - - Unsafe.CopyBlock(ref ulidBytesRef, ref lastUlidRef, _ulidSize); - } - finally - { - // Release the spinlock - Volatile.Write(ref _lastUlidLock, 0); } } @@ -297,77 +270,24 @@ private static void FillRandom(ref byte ulidBytesRef, long timestamp, Generation #else [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static void IncrementByOne(ref byte buffer) - { - const int lastIdx = _ulidSize - 1; - - ushort carry = 1; - ref var currentRef = ref Unsafe.Add(ref buffer, lastIdx); - - for (var i = lastIdx; i >= 0; i--) - { - var val = (ushort)(currentRef + carry); - currentRef = (byte)val; // Implicit & 0xFF - carry = (ushort)(val >> 8); - - if (carry == 0) - { - return; - } - - currentRef = ref Unsafe.Subtract(ref currentRef, 1); - } - - throw new OverflowException("Addition resulted in a value larger than the target span's capacity."); - } - -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - private static void IncrementByByteSpan(ref byte targetRef, ReadOnlySpan source) + private static void LastUlidIncrement(uint addition) { - ushort carry = 1; - ushort sum; - var lengthDifference = _ulidSize - source.Length; + var increment = (ulong)addition + 1; // carry = 1 is built-in + var part1 = ReverseOnLittleEndian(_state.LastUlidPart1); + var newPart1 = part1 + increment; + _state.LastUlidPart1 = ReverseOnLittleEndian(newPart1); - if (source.Length != 0) + if (newPart1 >= part1) { - for (var i = _ulidSize - 1; i >= lengthDifference; --i) - { - var sourceIdx = i - lengthDifference; - - ref var targetByteRef = ref Unsafe.Add(ref targetRef, i); - var byteFromSource = source[sourceIdx]; - - sum = (ushort)(targetByteRef + byteFromSource + carry); - targetByteRef = (byte)sum; // Implicit & 0xFF - carry = (byte)(sum >> 8); - } - - if (carry == 0) - { - return; - } + return; } - for (var i = lengthDifference - 1; i >= 0; --i) + var part0 = ReverseOnLittleEndian(_state.LastUlidPart0); + part0++; + _state.LastUlidPart0 = ReverseOnLittleEndian(part0); + if (part0 == 0) { - ref var targetByteRef = ref Unsafe.Add(ref targetRef, i); - sum = (ushort)(targetByteRef + carry); - targetByteRef = (byte)sum; // Implicit & 0xFF - carry = (ushort)(sum >> 8); - - if (carry == 0) - { - return; - } + throw new OverflowException("Addition resulted in a ULID value larger than the absolute maximum ULID value."); } - - throw new OverflowException("Addition resulted in a value larger than the target span's capacity."); } } \ No newline at end of file diff --git a/src/ByteAether.Ulid/Ulid.String.cs b/src/ByteAether.Ulid/Ulid.String.cs index f186911..5cedc47 100644 --- a/src/ByteAether.Ulid/Ulid.String.cs +++ b/src/ByteAether.Ulid/Ulid.String.cs @@ -212,7 +212,7 @@ private static unsafe Ulid ParseCore(ReadOnlySpan input) | (r02 << 1) | (r03 >> 4); - Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 0), BinaryPrimitives.ReverseEndianness(block1)); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 0), ReverseOnLittleEndian(block1)); // Second block - ulong 64 bits ulong r04 = Unsafe.Add(ref tableRef, pBytes[14 * stepSize]); @@ -243,7 +243,7 @@ private static unsafe Ulid ParseCore(ReadOnlySpan input) | (r14 << 5) | r15; - Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 8), BinaryPrimitives.ReverseEndianness(block2)); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 8), ReverseOnLittleEndian(block2)); } return result; @@ -429,7 +429,7 @@ private void Fill(Span span, T[] map) where T: unmanaged span[3] = map[((_t1 & 0x7) << 2) | (_t2 >> 6)]; // 00[11111111][11111|111][11|111111][11111111][11111111][11111111] span[2] = map[_t1 >> 3]; // 00[11111111][|11111|111][11111111][11111111][11111111][11111111] span[1] = map[_t0 & 0x1F]; // 00[111|11111|][11111111][11111111][11111111][11111111][11111111] - span[0] = map[_t0 >> 5]; // |00[111|11111][11111111][11111111][11111111][11111111][11111111] + span[0] = map[_t0 >> 5]; // |00[111|11111][11111111][11111111][11111111][11111111][11111111] } /// diff --git a/src/ByteAether.Ulid/Ulid.Util.cs b/src/ByteAether.Ulid/Ulid.Util.cs new file mode 100644 index 0000000..f3eef03 --- /dev/null +++ b/src/ByteAether.Ulid/Ulid.Util.cs @@ -0,0 +1,13 @@ +using System.Buffers.Binary; +using System.Runtime.CompilerServices; + +namespace ByteAether.Ulid; + +public readonly partial struct Ulid +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong ReverseOnLittleEndian(ulong value) + => BitConverter.IsLittleEndian + ? BinaryPrimitives.ReverseEndianness(value) + : value; +} \ No newline at end of file