From cbe711a17afe0fe3d942d6086bc20a2dc43c1684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonatan=20Uusv=C3=A4li?= Date: Sun, 14 Dec 2025 21:39:47 +0200 Subject: [PATCH 1/2] Improved timestamp handling in `Ulid.New` implementation, and added test for timestamp and random validation. Rewrote ULID generation methods to use refs instead of Spans for performance. Updated benchmark results in README --- README.md | 34 +-- .../ByteAether.Benchmarks.csproj | 2 +- .../ByteAether.Ulid.Tests.csproj | 4 +- src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs | 15 + .../Compatibility/MemoryMarshal.cs | 58 ++++ src/ByteAether.Ulid/Ulid.New.cs | 286 ++++++++++-------- 6 files changed, 261 insertions(+), 138 deletions(-) create mode 100644 src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs diff --git a/README.md b/README.md index e71c4d6..16c552b 100644 --- a/README.md +++ b/README.md @@ -398,28 +398,28 @@ The following benchmarks were performed: ``` BenchmarkDotNet v0.15.7, Windows 10 (10.0.19044.6575/21H2/November2021Update) AMD Ryzen 7 3700X 3.60GHz, 1 CPU, 12 logical and 6 physical cores -.NET SDK 10.0.100 - [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 - DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +.NET SDK 10.0.101 + [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + DefaultJob : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 Job=DefaultJob | Type | Method | Mean | Error | Gen0 | Allocated | |---------------- |------------------- |------------:|----------:|-------:|----------:| -| Generate | ByteAetherUlid | 46.1867 ns | 0.0914 ns | - | - | -| Generate | ByteAetherUlidR1Bp | 51.5996 ns | 0.1552 ns | - | - | -| Generate | ByteAetherUlidR4Bp | 56.5170 ns | 0.1043 ns | - | - | -| Generate | ByteAetherUlidR1Bc | 94.8500 ns | 0.2545 ns | - | - | -| Generate | ByteAetherUlidR4Bc | 100.9761 ns | 0.3672 ns | - | - | -| Generate | NetUlid *(1) | 159.2965 ns | 1.3950 ns | 0.0095 | 80 B | -| Generate | NUlid *(2) | 49.2036 ns | 0.1911 ns | - | - | - -| GenerateNonMono | ByteAetherUlid | 91.3682 ns | 0.2455 ns | - | - | -| GenerateNonMono | ByteAetherUlidP | 42.5785 ns | 0.1397 ns | - | - | -| GenerateNonMono | Ulid *(3,4) | 39.1454 ns | 0.0491 ns | - | - | -| GenerateNonMono | NUlid | 91.0387 ns | 0.2620 ns | - | - | -| GenerateNonMono | Guid *(5) | 48.1872 ns | 0.1581 ns | - | - | -| GenerateNonMono | GuidV7 *(3,5) | 77.2375 ns | 0.2567 ns | - | - | +| Generate | ByteAetherUlid | 44.39 ns | 0.854 ns | - | - | +| Generate | ByteAetherUlidR1Bp | 50.94 ns | 0.475 ns | - | - | +| Generate | ByteAetherUlidR4Bp | 53.19 ns | 0.393 ns | - | - | +| Generate | ByteAetherUlidR1Bc | 89.92 ns | 1.285 ns | - | - | +| Generate | ByteAetherUlidR4Bc | 98.13 ns | 1.901 ns | - | - | +| Generate | NetUlid *(1) | 161.74 ns | 2.706 ns | 0.0095 | 80 B | +| Generate | NUlid *(2) | 49.74 ns | 0.493 ns | - | - | + +| GenerateNonMono | ByteAetherUlid | 93.01 ns | 1.014 ns | - | - | +| GenerateNonMono | ByteAetherUlidP | 42.78 ns | 0.241 ns | - | - | +| GenerateNonMono | Ulid *(3,4) | 38.30 ns | 0.294 ns | - | - | +| GenerateNonMono | NUlid | 93.16 ns | 1.849 ns | - | - | +| GenerateNonMono | Guid *(5) | 48.20 ns | 0.372 ns | - | - | +| GenerateNonMono | GuidV7 *(3,5) | 78.10 ns | 0.488 ns | - | - | | FromByteArray | ByteAetherUlid | 0.0302 ns | 0.0045 ns | - | - | | FromByteArray | NetUlid | 0.7515 ns | 0.0083 ns | - | - | diff --git a/src/ByteAether.Ulid.Benchmarks/ByteAether.Benchmarks.csproj b/src/ByteAether.Ulid.Benchmarks/ByteAether.Benchmarks.csproj index 211b8b6..6f08bcf 100644 --- a/src/ByteAether.Ulid.Benchmarks/ByteAether.Benchmarks.csproj +++ b/src/ByteAether.Ulid.Benchmarks/ByteAether.Benchmarks.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/ByteAether.Ulid.Tests/ByteAether.Ulid.Tests.csproj b/src/ByteAether.Ulid.Tests/ByteAether.Ulid.Tests.csproj index f45945e..80bbe77 100644 --- a/src/ByteAether.Ulid.Tests/ByteAether.Ulid.Tests.csproj +++ b/src/ByteAether.Ulid.Tests/ByteAether.Ulid.Tests.csproj @@ -27,14 +27,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs b/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs index 072ac80..0e9a351 100644 --- a/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs +++ b/src/ByteAether.Ulid.Tests/Ulid.New.Tests.cs @@ -60,6 +60,21 @@ public void New_WithTimestampAndRandom_ShouldGenerateSameUlid() Assert.Equal(ulid1, ulid2); } + [Fact] + public void New_WithTimestampAndRandom_ShouldGenerateCorrectTimestampAndRandom() + { + // Arrange + var timestamp = DateTimeOffset.UtcNow; + var random = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + + // Act + var ulid = Ulid.New(timestamp, random); + + // Assert + Assert.Equal(timestamp.ToUnixTimeMilliseconds(), ulid.Time.ToUnixTimeMilliseconds()); + Assert.Equal(random, ulid.Random.ToArray()); + } + [Fact] public void New_NonMonotonic_CanProduceSmallerUlids() { diff --git a/src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs b/src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs new file mode 100644 index 0000000..eea970f --- /dev/null +++ b/src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs @@ -0,0 +1,58 @@ +#if NETSTANDARD +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// ReSharper disable All +#pragma warning disable + +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 ref T GetArrayDataReference(T[] array) => + ref GetArrayDataReference(array); + + /// + /// 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 + /// is not checked. + /// + /// A reference to data. + /// The number of elements the memory contains. + /// A 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 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/Ulid.New.cs b/src/ByteAether.Ulid/Ulid.New.cs index 48d7d17..268937c 100644 --- a/src/ByteAether.Ulid/Ulid.New.cs +++ b/src/ByteAether.Ulid/Ulid.New.cs @@ -19,6 +19,9 @@ public readonly partial struct Ulid private static readonly byte[] _lastUlid = new byte[_ulidSize]; + // Constant for Unix Epoch (1970-01-01 UTC) in Ticks + private const long _unixEpochTicks = 621355968000000000; + /// /// Initializes a new instance of the struct using the specified byte array. /// @@ -46,7 +49,9 @@ public static Ulid New(ReadOnlySpan bytes) [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Ulid New(GenerationOptions? options = null) - => New(DateTimeOffset.UtcNow, options); + // We can avoid Offset-related allocations by using DateTime over DateTimeOffset + // For public API, DateTimeOffset is an official recommendation + => New((DateTime.UtcNow.Ticks - _unixEpochTicks) / TimeSpan.TicksPerMillisecond, options); /// /// Creates a new with the specified timestamp. @@ -103,13 +108,19 @@ public static Ulid New(long timestamp, GenerationOptions? options = null) { Ulid ulid = default; - unsafe - { - var ulidBytes = new Span(Unsafe.AsPointer(ref Unsafe.AsRef(in ulid)), _ulidSize); + ref var ulidRef = ref Unsafe.As(ref ulid); - FillTime(ulidBytes, timestamp); - FillRandom(ulidBytes, options ?? DefaultGenerationOptions); - } + // Fill timestamp + BinaryPrimitives.WriteUInt64BigEndian( +#if NETCOREAPP + MemoryMarshal.CreateSpan(ref ulidRef, 8), +#else + Compatibility.MemoryMarshal.CreateSpan(ref ulidRef, 8), +#endif + (ulong)timestamp << 16 + ); + + FillRandom(ref ulidRef, timestamp, options ?? DefaultGenerationOptions); return ulid; } @@ -140,33 +151,25 @@ public static Ulid New(long timestamp, Span random) { Ulid ulid = default; - unsafe - { - var ulidBytes = new Span(Unsafe.AsPointer(ref Unsafe.AsRef(in ulid)), _ulidSize); - - FillTime(ulidBytes, timestamp); - random.CopyTo(ulidBytes[_ulidSizeTime..]); - } - - return ulid; - } + ref var ulidRef = ref Unsafe.As(ref ulid); -#if NET5_0_OR_GREATER - [SkipLocalsInit] -#endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + // Fill timestamp + BinaryPrimitives.WriteUInt64BigEndian( +#if NETCOREAPP + MemoryMarshal.CreateSpan(ref ulidRef, 8), #else - [MethodImpl(MethodImplOptions.AggressiveInlining)] + Compatibility.MemoryMarshal.CreateSpan(ref ulidRef, 8), #endif - private static void FillTime(Span bytes, long timestamp) - { - bytes[0] = (byte)((timestamp >> 40) & 0xFF); - bytes[1] = (byte)((timestamp >> 32) & 0xFF); - bytes[2] = (byte)((timestamp >> 24) & 0xFF); - bytes[3] = (byte)((timestamp >> 16) & 0xFF); - bytes[4] = (byte)((timestamp >> 8) & 0xFF); - bytes[5] = (byte)( timestamp & 0xFF); + (ulong)timestamp << 16 + ); + + Unsafe.CopyBlockUnaligned( + ref Unsafe.Add(ref ulidRef, _ulidSizeTime), + ref random.GetPinnableReference(), + _ulidSizeRandom + ); + + return ulid; } private static int _lastUlidLock; @@ -179,67 +182,89 @@ private static void FillTime(Span bytes, long timestamp) #else [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static void FillRandom(Span bytes, GenerationOptions options) + private static void FillRandom(ref byte ulidBytesRef, long timestamp, GenerationOptions options) { - if (options.Monotonicity == GenerationOptions.MonotonicityOptions.NonMonotonic) - { - options.InitialRandomSource.GetBytes(bytes[_ulidSizeTime..]); - return; - } - - var lastUlidSpan = _lastUlid.AsSpan(); - var currentTime = ReadTimestamp48BigEndian(bytes); - - // Acquire lightweight spinlock - AcquireSpinLock(); - try - { - var lastTime = ReadTimestamp48BigEndian(lastUlidSpan); - // If the timestamp is the same or lesser than the last one, increment the last ULID by one - if (currentTime <= lastTime) - { - // We can use the last bytes of incomplete ULID for the increment parameter - var randomByteCount = (int)options.Monotonicity; - var tempSpan = bytes.Slice(_ulidSize - randomByteCount, randomByteCount); - - if (randomByteCount > 0) - { - options.IncrementRandomSource.GetBytes(tempSpan); - } - - IncrementByteSpan(lastUlidSpan, tempSpan); - } - // Otherwise, generate a new ULID - else - { - bytes[.._ulidSizeTime].CopyTo(lastUlidSpan); - options.InitialRandomSource.GetBytes(lastUlidSpan[_ulidSizeTime..]); - } + // Calculate offset to a random part + ref var randomPartRef = ref Unsafe.Add(ref ulidBytesRef, _ulidSizeTime); + + if (options.Monotonicity == GenerationOptions.MonotonicityOptions.NonMonotonic) + { + options.InitialRandomSource.GetBytes( +#if NETCOREAPP + MemoryMarshal.CreateSpan(ref randomPartRef, _ulidSizeRandom) +#else + Compatibility.MemoryMarshal.CreateSpan(ref randomPartRef, _ulidSizeRandom) +#endif + ); + return; + } - _lastUlid.CopyTo(bytes); - } - finally - { - // Release the spinlock - Volatile.Write(ref _lastUlidLock, 0); - } - } + ref var lastUlidRef = +#if NETCOREAPP + ref MemoryMarshal.GetArrayDataReference(_lastUlid); +#else + ref Compatibility.MemoryMarshal.GetArrayDataReference(_lastUlid); +#endif -#if NET5_0_OR_GREATER - [SkipLocalsInit] + ref var lastRandomRef = ref Unsafe.Add(ref lastUlidRef, _ulidSizeTime); + + AcquireSpinLock(); + try + { + // Read the last timestamp raw (from bytes 0-7 of lastUlid) + // Shift it to get 48 bits. + var lastTime = (long)( + BinaryPrimitives.ReadUInt64BigEndian( +#if NETCOREAPP + MemoryMarshal.CreateReadOnlySpan(ref lastUlidRef, sizeof(ulong)) +#else + Compatibility.MemoryMarshal.CreateReadOnlySpan(ref lastUlidRef, sizeof(ulong)) #endif -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + ) >> 16 + ); + + // If the timestamp is the same or lesser than the last one, increment the last ULID by one + if (timestamp <= lastTime) + { + if (options.Monotonicity == GenerationOptions.MonotonicityOptions.MonotonicIncrement) + { + IncrementByOne(ref lastUlidRef); + } + 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); #else - [MethodImpl(MethodImplOptions.AggressiveInlining)] + Compatibility.MemoryMarshal.CreateSpan(ref randomPartRef, (int)options.Monotonicity); #endif - private static ulong ReadTimestamp48BigEndian(ReadOnlySpan bytes) - { - // We can always call ReverseEndianness - it becomes no-op by JIT on BE systems. - var val = BinaryPrimitives.ReverseEndianness( - Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(bytes)) - ); - return val & 0x0000FFFFFFFFFFFFUL; + options.IncrementRandomSource.GetBytes(tempSpan); + IncrementByByteSpan(ref lastUlidRef, tempSpan); + } + } + else // Otherwise, generate a new ULID + { + // Copy timestamp from the incomplete ULID + Unsafe.CopyBlockUnaligned(ref lastUlidRef, ref ulidBytesRef, _ulidSizeTime); + + // Generate a new random to the last ULID + options.InitialRandomSource.GetBytes( +#if NETCOREAPP + MemoryMarshal.CreateSpan(ref lastRandomRef, _ulidSize) +#else + Compatibility.MemoryMarshal.CreateSpan(ref lastRandomRef, _ulidSize) +#endif + ); + } + + Unsafe.CopyBlockUnaligned(ref ulidBytesRef, ref lastUlidRef, _ulidSize); + } + finally + { + // Release the spinlock + Volatile.Write(ref _lastUlidLock, 0); + } } #if NET5_0_OR_GREATER @@ -250,20 +275,28 @@ private static ulong ReadTimestamp48BigEndian(ReadOnlySpan bytes) #else [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static void AcquireSpinLock() + private static void IncrementByOne(ref byte buffer) { - // Hot-path - if (Interlocked.CompareExchange(ref _lastUlidLock, 1, 0) == 0) - { - return; - } + const int lastIdx = _ulidSize - 1; - // Spin until the lock is acquired - var spinner = new SpinWait(); - while (Interlocked.CompareExchange(ref _lastUlidLock, 1, 0) == 1) + ushort carry = 1; + ref var currentRef = ref Unsafe.Add(ref buffer, lastIdx); + + for (var i = lastIdx; i >= 0; i--) { - spinner.SpinOnce(); + 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 @@ -274,27 +307,24 @@ private static void AcquireSpinLock() #else [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static void IncrementByteSpan(Span targetSpan, ReadOnlySpan sourceSpan) + private static void IncrementByByteSpan(ref byte targetRef, ReadOnlySpan source) { - ushort carry = 1; // max sum 255 + 255 + 1 = 511; guarantee at least +1 - - // This value represents the offset from the start of targetSpan to the start of sourceSpan - var lengthDifference = targetSpan.Length - sourceSpan.Length; - ushort sum; // Per-byte additions placeholder + ushort carry = 1; + ushort sum; + var lengthDifference = _ulidSize - source.Length; - // Phase 1: Process the common length part (where both targetSpan and sourceSpan contribute) - if (sourceSpan.Length != 0) + if (source.Length != 0) { - for (var i = targetSpan.Length - 1; i >= lengthDifference; --i) + for (var i = _ulidSize - 1; i >= lengthDifference; --i) { var sourceIdx = i - lengthDifference; - var byteFromTarget = targetSpan[i]; - var byteFromSource = sourceSpan[sourceIdx]; + ref var targetByteRef = ref Unsafe.Add(ref targetRef, i); + var byteFromSource = source[sourceIdx]; - sum = (ushort)(byteFromTarget + byteFromSource + carry); - targetSpan[i] = (byte)(sum & 0xFF); - carry = (ushort)(sum >> 8); + sum = (ushort)(targetByteRef + byteFromSource + carry); + targetByteRef = (byte)sum; // Implicit & 0xFF + carry = (byte)(sum >> 8); } if (carry == 0) @@ -303,23 +333,43 @@ private static void IncrementByteSpan(Span targetSpan, ReadOnlySpan } } - // Phase 2: Process the remaining part of targetSpan (only carry propagation) - // Runs from the point where sourceSpan ended, towards the MSB end of targetSpan for (var i = lengthDifference - 1; i >= 0; --i) { - var byteFromTarget = targetSpan[i]; - sum = (ushort)(byteFromTarget + carry); - targetSpan[i] = (byte)(sum & 0xFF); + 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; // No more carry to propagate + return; } } - // If there's still a carry (we have not returned from the method), - // it indicates an overflow beyond the original targetSpan's capacity. 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 AcquireSpinLock() + { + // 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(); + } + } } \ No newline at end of file From 2bb7d3d604fc6be5deac5ad382b206f058128568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joonatan=20Uusv=C3=A4li?= Date: Mon, 15 Dec 2025 15:42:16 +0200 Subject: [PATCH 2/2] Refactor ULID parsing/encoding to use Unsafe.As/Add for direct memory access, improving performance and eliminating bounds checks. Updated benchmark results in README. --- README.md | 130 ++++++++-------- .../Compatibility/MemoryMarshal.cs | 96 ++++++------ .../Compatibility/RuntimeHelpers.cs | 49 ------ src/ByteAether.Ulid/Ulid.New.cs | 4 +- src/ByteAether.Ulid/Ulid.Obsolete.cs | 2 +- src/ByteAether.Ulid/Ulid.String.cs | 142 ++++++++---------- 6 files changed, 186 insertions(+), 237 deletions(-) delete mode 100644 src/ByteAether.Ulid/Compatibility/RuntimeHelpers.cs diff --git a/README.md b/README.md index 16c552b..e69066b 100644 --- a/README.md +++ b/README.md @@ -396,7 +396,7 @@ Benchmark scenarios also include comparisons against `Guid`, where functionality The following benchmarks were performed: ``` -BenchmarkDotNet v0.15.7, Windows 10 (10.0.19044.6575/21H2/November2021Update) +BenchmarkDotNet v0.15.8, Windows 10 (10.0.19044.6691/21H2/November2021Update) AMD Ryzen 7 3700X 3.60GHz, 1 CPU, 12 logical and 6 physical cores .NET SDK 10.0.101 [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 @@ -406,70 +406,70 @@ Job=DefaultJob | Type | Method | Mean | Error | Gen0 | Allocated | |---------------- |------------------- |------------:|----------:|-------:|----------:| -| Generate | ByteAetherUlid | 44.39 ns | 0.854 ns | - | - | -| Generate | ByteAetherUlidR1Bp | 50.94 ns | 0.475 ns | - | - | -| Generate | ByteAetherUlidR4Bp | 53.19 ns | 0.393 ns | - | - | -| Generate | ByteAetherUlidR1Bc | 89.92 ns | 1.285 ns | - | - | -| Generate | ByteAetherUlidR4Bc | 98.13 ns | 1.901 ns | - | - | -| Generate | NetUlid *(1) | 161.74 ns | 2.706 ns | 0.0095 | 80 B | -| Generate | NUlid *(2) | 49.74 ns | 0.493 ns | - | - | - -| GenerateNonMono | ByteAetherUlid | 93.01 ns | 1.014 ns | - | - | -| GenerateNonMono | ByteAetherUlidP | 42.78 ns | 0.241 ns | - | - | -| GenerateNonMono | Ulid *(3,4) | 38.30 ns | 0.294 ns | - | - | -| GenerateNonMono | NUlid | 93.16 ns | 1.849 ns | - | - | -| GenerateNonMono | Guid *(5) | 48.20 ns | 0.372 ns | - | - | -| GenerateNonMono | GuidV7 *(3,5) | 78.10 ns | 0.488 ns | - | - | - -| FromByteArray | ByteAetherUlid | 0.0302 ns | 0.0045 ns | - | - | -| FromByteArray | NetUlid | 0.7515 ns | 0.0083 ns | - | - | -| FromByteArray | Ulid | 0.0263 ns | 0.0036 ns | - | - | -| FromByteArray | NUlid | 0.0439 ns | 0.0130 ns | - | - | -| FromByteArray | Guid | 0.0397 ns | 0.0131 ns | - | - | - -| FromGuid | ByteAetherUlid | 0.0533 ns | 0.0216 ns | - | - | -| FromGuid | NetUlid | 3.2410 ns | 0.0616 ns | - | - | -| FromGuid | Ulid | 1.7144 ns | 0.0092 ns | - | - | -| FromGuid | NUlid | 0.5250 ns | 0.0085 ns | - | - | - -| FromString | ByteAetherUlid | 14.7203 ns | 0.0557 ns | - | - | -| FromString | NetUlid | 26.7763 ns | 0.1008 ns | - | - | -| FromString | Ulid | 14.6891 ns | 0.0547 ns | - | - | -| FromString | NUlid | 51.7824 ns | 0.1617 ns | 0.0086 | 72 B | -| FromString | Guid | 20.0958 ns | 0.1317 ns | - | - | - -| ToByteArray | ByteAetherUlid | 4.1367 ns | 0.1012 ns | 0.0048 | 40 B | -| ToByteArray | NetUlid | 9.4660 ns | 0.1082 ns | 0.0048 | 40 B | -| ToByteArray | Ulid | 4.0211 ns | 0.0809 ns | 0.0048 | 40 B | -| ToByteArray | NUlid | 4.2021 ns | 0.0903 ns | 0.0048 | 40 B | - -| ToGuid | ByteAetherUlid | 0.2625 ns | 0.0051 ns | - | - | -| ToGuid | NetUlid | 10.3348 ns | 0.0326 ns | - | - | -| ToGuid | Ulid | 0.7462 ns | 0.0117 ns | - | - | -| ToGuid | NUlid | 0.2691 ns | 0.0070 ns | - | - | - -| ToString | ByteAetherUlid | 12.254 ns | 0.2822 ns | 0.0096 | 80 B | -| ToString | NetUlid | 26.314 ns | 0.2748 ns | 0.0095 | 80 B | -| ToString | Ulid | 12.373 ns | 0.1887 ns | 0.0096 | 80 B | -| ToString | NUlid | 27.661 ns | 0.2090 ns | 0.0095 | 80 B | -| ToString | Guid | 7.208 ns | 0.0447 ns | 0.0115 | 96 B | - -| CompareTo | ByteAetherUlid | 0.0007 ns | 0.0022 ns | - | - | -| CompareTo | NetUlid | 3.6812 ns | 0.0298 ns | - | - | -| CompareTo | Ulid | 0.0002 ns | 0.0006 ns | - | - | -| CompareTo | NUlid | 0.4122 ns | 0.0062 ns | - | - | - -| Equals | ByteAetherUlid | 0.0016 ns | 0.0028 ns | - | - | -| Equals | NetUlid | 1.0102 ns | 0.0059 ns | - | - | -| Equals | Ulid | 0.0011 ns | 0.0020 ns | - | - | -| Equals | NUlid | 0.0000 ns | 0.0000 ns | - | - | -| Equals | Guid | 0.0010 ns | 0.0023 ns | - | - | - -| GetHashCode | ByteAetherUlid | 0.0008 ns | 0.0018 ns | - | - | -| GetHashCode | NetUlid | 9.8270 ns | 0.0271 ns | - | - | -| GetHashCode | Ulid | 0.0032 ns | 0.0032 ns | - | - | -| GetHashCode | NUlid | 5.7843 ns | 0.0235 ns | - | - | -| GetHashCode | Guid | 0.0016 ns | 0.0028 ns | - | - | +| Generate | ByteAetherUlid | 42.7482 ns | 0.1075 ns | - | - | +| Generate | ByteAetherUlidR1Bp | 48.1939 ns | 0.3909 ns | - | - | +| Generate | ByteAetherUlidR4Bp | 52.3962 ns | 0.1214 ns | - | - | +| Generate | ByteAetherUlidR1Bc | 91.2941 ns | 0.2795 ns | - | - | +| Generate | ByteAetherUlidR4Bc | 99.4539 ns | 0.4657 ns | - | - | +| Generate | NetUlid *(1) | 158.9262 ns | 1.0281 ns | 0.0095 | 80 B | +| Generate | NUlid *(2) | 50.0544 ns | 0.2260 ns | - | - | + +| GenerateNonMono | ByteAetherUlid | 91.3593 ns | 0.3431 ns | - | - | +| GenerateNonMono | ByteAetherUlidP | 41.9809 ns | 0.1385 ns | - | - | +| GenerateNonMono | Ulid *(3,4) | 39.9820 ns | 0.2004 ns | - | - | +| GenerateNonMono | NUlid | 92.1577 ns | 0.4041 ns | - | - | +| GenerateNonMono | Guid *(5) | 48.5804 ns | 0.1707 ns | - | - | +| GenerateNonMono | GuidV7 *(3,5) | 79.2241 ns | 0.3652 ns | - | - | + +| FromByteArray | ByteAetherUlid | 0.0211 ns | 0.0030 ns | - | - | +| FromByteArray | NetUlid | 0.6503 ns | 0.0081 ns | - | - | +| FromByteArray | Ulid | 0.2572 ns | 0.0030 ns | - | - | +| FromByteArray | NUlid | 0.0104 ns | 0.0068 ns | - | - | +| FromByteArray | Guid | 0.0193 ns | 0.0041 ns | - | - | + +| FromGuid | ByteAetherUlid | 0.0138 ns | 0.0107 ns | - | - | +| FromGuid | NetUlid | 1.2947 ns | 0.0436 ns | - | - | +| FromGuid | Ulid | 1.7548 ns | 0.0164 ns | - | - | +| FromGuid | NUlid | 0.5480 ns | 0.0344 ns | - | - | + +| FromString | ByteAetherUlid | 14.2074 ns | 0.2594 ns | - | - | +| FromString | NetUlid | 28.0119 ns | 0.5557 ns | - | - | +| FromString | Ulid | 15.3590 ns | 0.2057 ns | - | - | +| FromString | NUlid | 53.5128 ns | 0.3265 ns | 0.0086 | 72 B | +| FromString | Guid | 20.6887 ns | 0.2020 ns | - | - | + +| ToByteArray | ByteAetherUlid | 4.5071 ns | 0.1399 ns | 0.0048 | 40 B | +| ToByteArray | NetUlid | 10.4349 ns | 0.2341 ns | 0.0048 | 40 B | +| ToByteArray | Ulid | 4.1026 ns | 0.1328 ns | 0.0048 | 40 B | +| ToByteArray | NUlid | 4.4312 ns | 0.1437 ns | 0.0048 | 40 B | + +| ToGuid | ByteAetherUlid | 0.2604 ns | 0.0043 ns | - | - | +| ToGuid | NetUlid | 10.4844 ns | 0.0151 ns | - | - | +| ToGuid | Ulid | 0.7432 ns | 0.0079 ns | - | - | +| ToGuid | NUlid | 0.2733 ns | 0.0056 ns | - | - | + +| ToString | ByteAetherUlid | 12.4495 ns | 0.2869 ns | 0.0096 | 80 B | +| ToString | NetUlid | 24.2338 ns | 0.3168 ns | 0.0095 | 80 B | +| ToString | Ulid | 12.4809 ns | 0.2004 ns | 0.0096 | 80 B | +| ToString | NUlid | 29.8794 ns | 0.0559 ns | 0.0095 | 80 B | +| ToString | Guid | 7.9268 ns | 0.0546 ns | 0.0115 | 96 B | + +| CompareTo | ByteAetherUlid | 0.0002 ns | 0.0005 ns | - | - | +| CompareTo | NetUlid | 3.7498 ns | 0.0348 ns | - | - | +| CompareTo | Ulid | 0.0034 ns | 0.0033 ns | - | - | +| CompareTo | NUlid | 0.3966 ns | 0.0080 ns | - | - | + +| Equals | ByteAetherUlid | 0.0019 ns | 0.0022 ns | - | - | +| Equals | NetUlid | 1.0192 ns | 0.0189 ns | - | - | +| Equals | Ulid | 0.0001 ns | 0.0003 ns | - | - | +| Equals | NUlid | 0.0111 ns | 0.0123 ns | - | - | +| Equals | Guid | 0.0013 ns | 0.0017 ns | - | - | + +| GetHashCode | ByteAetherUlid | 0.0000 ns | 0.0000 ns | - | - | +| GetHashCode | NetUlid | 9.9751 ns | 0.0332 ns | - | - | +| GetHashCode | Ulid | 0.0001 ns | 0.0006 ns | - | - | +| GetHashCode | NUlid | 6.0511 ns | 0.0297 ns | - | - | +| GetHashCode | Guid | 0.0002 ns | 0.0008 ns | - | - | ``` Existing competitive libraries exhibit various deviations from the official ULID specification or present drawbacks: diff --git a/src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs b/src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs index eea970f..8e7463b 100644 --- a/src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs +++ b/src/ByteAether.Ulid/Compatibility/MemoryMarshal.cs @@ -1,58 +1,66 @@ #if NETSTANDARD using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; // ReSharper disable All #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 ref T GetArrayDataReference(T[] array) => - ref GetArrayDataReference(array); + /// + /// 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 - /// is not checked. - /// - /// A reference to data. - /// The number of elements the memory contains. - /// A 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 Span CreateSpan(scoped ref T reference, int length) => - new Span(Unsafe.AsPointer(ref reference), length); + /// + /// 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 + /// is not checked. + /// + /// A reference to data. + /// The number of elements the memory contains. + /// A 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 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); + /// + /// 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/Compatibility/RuntimeHelpers.cs b/src/ByteAether.Ulid/Compatibility/RuntimeHelpers.cs deleted file mode 100644 index 89adf46..0000000 --- a/src/ByteAether.Ulid/Compatibility/RuntimeHelpers.cs +++ /dev/null @@ -1,49 +0,0 @@ -#if NETSTANDARD2_0 - -// ReSharper disable All -#pragma warning disable -// https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/Common/tests/System/Runtime/CompilerServices/RuntimeHelpers.cs - -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Runtime.CompilerServices -{ - internal static class RuntimeHelpers - { - /// - /// Slices the specified array using the specified range. - /// - internal static T[] GetSubArray(T[] array, Range range) - { - if (array == null) - { - throw new ArgumentNullException(nameof(array)); - } - - (int offset, int length) = range.GetOffsetAndLength(array.Length); - - if (default(T) != null || typeof(T[]) == array.GetType()) - { - // We know the type of the array to be exactly T[]. - - if (length == 0) - { - return Array.Empty(); - } - - var dest = new T[length]; - Array.Copy(array, offset, dest, 0, length); - return dest; - } - else - { - // The array is actually a U[] where U:T. - var dest = (T[])Array.CreateInstance(array.GetType().GetElementType(), length); - Array.Copy(array, offset, dest, 0, length); - return dest; - } - } - } -} -#endif \ No newline at end of file diff --git a/src/ByteAether.Ulid/Ulid.New.cs b/src/ByteAether.Ulid/Ulid.New.cs index 268937c..fce2f0b 100644 --- a/src/ByteAether.Ulid/Ulid.New.cs +++ b/src/ByteAether.Ulid/Ulid.New.cs @@ -246,7 +246,7 @@ private static void FillRandom(ref byte ulidBytesRef, long timestamp, Generation else // Otherwise, generate a new ULID { // Copy timestamp from the incomplete ULID - Unsafe.CopyBlockUnaligned(ref lastUlidRef, ref ulidBytesRef, _ulidSizeTime); + Unsafe.CopyBlock(ref lastUlidRef, ref ulidBytesRef, _ulidSizeTime); // Generate a new random to the last ULID options.InitialRandomSource.GetBytes( @@ -258,7 +258,7 @@ private static void FillRandom(ref byte ulidBytesRef, long timestamp, Generation ); } - Unsafe.CopyBlockUnaligned(ref ulidBytesRef, ref lastUlidRef, _ulidSize); + Unsafe.CopyBlock(ref ulidBytesRef, ref lastUlidRef, _ulidSize); } finally { diff --git a/src/ByteAether.Ulid/Ulid.Obsolete.cs b/src/ByteAether.Ulid/Ulid.Obsolete.cs index 8b2d984..c91513c 100644 --- a/src/ByteAether.Ulid/Ulid.Obsolete.cs +++ b/src/ByteAether.Ulid/Ulid.Obsolete.cs @@ -77,7 +77,7 @@ internal static class ObsoleteHelper }; [MethodImpl(MethodImplOptions.AggressiveInlining)] - [return: NotNullIfNotNull("isMonotonic")] + [return: NotNullIfNotNull(nameof(isMonotonic))] public static Ulid.GenerationOptions? GetByBoolean(bool? isMonotonic) => isMonotonic switch { diff --git a/src/ByteAether.Ulid/Ulid.String.cs b/src/ByteAether.Ulid/Ulid.String.cs index f2d8d43..a9d49c9 100644 --- a/src/ByteAether.Ulid/Ulid.String.cs +++ b/src/ByteAether.Ulid/Ulid.String.cs @@ -122,34 +122,30 @@ public static Ulid Parse(ReadOnlySpan chars, IFormatProvider? provider = n // Sanity check. if (chars.Length != UlidStringLength) { - throw new FormatException(); + throw new FormatException("The input char sequence is not a valid ULID string representation."); } // Decode. Ulid ulid = default; - unsafe - { - var ulidBytes = new Span(Unsafe.AsPointer(ref Unsafe.AsRef(in ulid)), _ulidSize); - - ulidBytes[15] = (byte)((_inverseBase32[chars[24]] << 5) | _inverseBase32[chars[25]]); - - ulidBytes[00] = (byte)((_inverseBase32[chars[0]] << 5) | _inverseBase32[chars[1]]); - ulidBytes[01] = (byte)((_inverseBase32[chars[2]] << 3) | (_inverseBase32[chars[3]] >> 2)); - ulidBytes[02] = (byte)((_inverseBase32[chars[3]] << 6) | (_inverseBase32[chars[4]] << 1) | (_inverseBase32[chars[5]] >> 4)); - ulidBytes[03] = (byte)((_inverseBase32[chars[5]] << 4) | (_inverseBase32[chars[6]] >> 1)); - ulidBytes[04] = (byte)((_inverseBase32[chars[6]] << 7) | (_inverseBase32[chars[7]] << 2) | (_inverseBase32[chars[8]] >> 3)); - ulidBytes[05] = (byte)((_inverseBase32[chars[8]] << 5) | _inverseBase32[chars[9]]); - ulidBytes[06] = (byte)((_inverseBase32[chars[10]] << 3) | (_inverseBase32[chars[11]] >> 2)); - ulidBytes[07] = (byte)((_inverseBase32[chars[11]] << 6) | (_inverseBase32[chars[12]] << 1) | (_inverseBase32[chars[13]] >> 4)); - ulidBytes[08] = (byte)((_inverseBase32[chars[13]] << 4) | (_inverseBase32[chars[14]] >> 1)); - ulidBytes[09] = (byte)((_inverseBase32[chars[14]] << 7) | (_inverseBase32[chars[15]] << 2) | (_inverseBase32[chars[16]] >> 3)); - ulidBytes[10] = (byte)((_inverseBase32[chars[16]] << 5) | _inverseBase32[chars[17]]); - ulidBytes[11] = (byte)((_inverseBase32[chars[18]] << 3) | (_inverseBase32[chars[19]] >> 2)); - ulidBytes[12] = (byte)((_inverseBase32[chars[19]] << 6) | (_inverseBase32[chars[20]] << 1) | (_inverseBase32[chars[21]] >> 4)); - ulidBytes[13] = (byte)((_inverseBase32[chars[21]] << 4) | (_inverseBase32[chars[22]] >> 1)); - ulidBytes[14] = (byte)((_inverseBase32[chars[22]] << 7) | (_inverseBase32[chars[23]] << 2) | (_inverseBase32[chars[24]] >> 3)); - } + ref var ulidRef = ref Unsafe.As(ref ulid); + + Unsafe.Add(ref ulidRef, 15) = (byte)(_inverseBase32[chars[25]] | (_inverseBase32[chars[24]] << 5)); + Unsafe.Add(ref ulidRef, 14) = (byte)((_inverseBase32[chars[24]] >> 3) | (_inverseBase32[chars[23]] << 2) | (_inverseBase32[chars[22]] << 7)); + Unsafe.Add(ref ulidRef, 13) = (byte)((_inverseBase32[chars[22]] >> 1) | (_inverseBase32[chars[21]] << 4)); + Unsafe.Add(ref ulidRef, 12) = (byte)((_inverseBase32[chars[21]] >> 4) | (_inverseBase32[chars[20]] << 1) | (_inverseBase32[chars[19]] << 6)); + Unsafe.Add(ref ulidRef, 11) = (byte)((_inverseBase32[chars[19]] >> 2) | (_inverseBase32[chars[18]] << 3)); + Unsafe.Add(ref ulidRef, 10) = (byte)(_inverseBase32[chars[17]] | (_inverseBase32[chars[16]] << 5)); + Unsafe.Add(ref ulidRef, 09) = (byte)((_inverseBase32[chars[16]] >> 3) | (_inverseBase32[chars[15]] << 2) | (_inverseBase32[chars[14]] << 7)); + Unsafe.Add(ref ulidRef, 08) = (byte)((_inverseBase32[chars[14]] >> 1) | (_inverseBase32[chars[13]] << 4)); + Unsafe.Add(ref ulidRef, 07) = (byte)((_inverseBase32[chars[13]] >> 4) | (_inverseBase32[chars[12]] << 1) | (_inverseBase32[chars[11]] << 6)); + Unsafe.Add(ref ulidRef, 06) = (byte)((_inverseBase32[chars[11]] >> 2) | (_inverseBase32[chars[10]] << 3)); + Unsafe.Add(ref ulidRef, 05) = (byte)(_inverseBase32[chars[9]] | (_inverseBase32[chars[8]] << 5)); + Unsafe.Add(ref ulidRef, 04) = (byte)((_inverseBase32[chars[8]] >> 3) | (_inverseBase32[chars[7]] << 2) | (_inverseBase32[chars[6]] << 7)); + Unsafe.Add(ref ulidRef, 03) = (byte)((_inverseBase32[chars[6]] >> 1) | (_inverseBase32[chars[5]] << 4)); + Unsafe.Add(ref ulidRef, 02) = (byte)((_inverseBase32[chars[5]] >> 4) | (_inverseBase32[chars[4]] << 1) | (_inverseBase32[chars[3]] << 6)); + Unsafe.Add(ref ulidRef, 01) = (byte)((_inverseBase32[chars[3]] >> 2) | (_inverseBase32[chars[2]] << 3)); + Unsafe.Add(ref ulidRef, 00) = (byte)(_inverseBase32[chars[1]] | (_inverseBase32[chars[0]] << 5)); return ulid; } @@ -157,7 +153,7 @@ public static Ulid Parse(ReadOnlySpan chars, IFormatProvider? provider = n /// /// Parses a ULID from a read-only span of bytes and returns the corresponding ULID value. /// - /// The read-only span of bytes containing the ULID string representation in Crockford's Base32 format. + /// The read-only span of bytes containing the ULID string representation in Crockford's Base32 format. /// Ignored. The ULID is always formatted in its canonical Crockford's Base32 format. /// The ULID parsed from the specified byte span. /// Thrown if the input byte span does not contain a valid ULID string representation. @@ -167,39 +163,35 @@ public static Ulid Parse(ReadOnlySpan chars, IFormatProvider? provider = n #if NETCOREAPP3_0_OR_GREATER [MethodImpl(MethodImplOptions.AggressiveOptimization)] #endif - public static Ulid Parse(ReadOnlySpan chars, IFormatProvider? provider = null) + public static Ulid Parse(ReadOnlySpan bytes, IFormatProvider? provider = null) { // Sanity check. - if (chars.Length != UlidStringLength) + if (bytes.Length != UlidStringLength) { - throw new FormatException(); + throw new FormatException("The input byte sequence is not a valid ULID string representation."); } // Decode. Ulid ulid = default; - unsafe - { - var ulidBytes = new Span(Unsafe.AsPointer(ref Unsafe.AsRef(in ulid)), _ulidSize); - - ulidBytes[15] = (byte)((_inverseBase32[chars[24]] << 5) | _inverseBase32[chars[25]]); - - ulidBytes[00] = (byte)((_inverseBase32[chars[0]] << 5) | _inverseBase32[chars[1]]); - ulidBytes[01] = (byte)((_inverseBase32[chars[2]] << 3) | (_inverseBase32[chars[3]] >> 2)); - ulidBytes[02] = (byte)((_inverseBase32[chars[3]] << 6) | (_inverseBase32[chars[4]] << 1) | (_inverseBase32[chars[5]] >> 4)); - ulidBytes[03] = (byte)((_inverseBase32[chars[5]] << 4) | (_inverseBase32[chars[6]] >> 1)); - ulidBytes[04] = (byte)((_inverseBase32[chars[6]] << 7) | (_inverseBase32[chars[7]] << 2) | (_inverseBase32[chars[8]] >> 3)); - ulidBytes[05] = (byte)((_inverseBase32[chars[8]] << 5) | _inverseBase32[chars[9]]); - ulidBytes[06] = (byte)((_inverseBase32[chars[10]] << 3) | (_inverseBase32[chars[11]] >> 2)); - ulidBytes[07] = (byte)((_inverseBase32[chars[11]] << 6) | (_inverseBase32[chars[12]] << 1) | (_inverseBase32[chars[13]] >> 4)); - ulidBytes[08] = (byte)((_inverseBase32[chars[13]] << 4) | (_inverseBase32[chars[14]] >> 1)); - ulidBytes[09] = (byte)((_inverseBase32[chars[14]] << 7) | (_inverseBase32[chars[15]] << 2) | (_inverseBase32[chars[16]] >> 3)); - ulidBytes[10] = (byte)((_inverseBase32[chars[16]] << 5) | _inverseBase32[chars[17]]); - ulidBytes[11] = (byte)((_inverseBase32[chars[18]] << 3) | (_inverseBase32[chars[19]] >> 2)); - ulidBytes[12] = (byte)((_inverseBase32[chars[19]] << 6) | (_inverseBase32[chars[20]] << 1) | (_inverseBase32[chars[21]] >> 4)); - ulidBytes[13] = (byte)((_inverseBase32[chars[21]] << 4) | (_inverseBase32[chars[22]] >> 1)); - ulidBytes[14] = (byte)((_inverseBase32[chars[22]] << 7) | (_inverseBase32[chars[23]] << 2) | (_inverseBase32[chars[24]] >> 3)); - } + ref var ulidRef = ref Unsafe.As(ref ulid); + + Unsafe.Add(ref ulidRef, 15) = (byte)(_inverseBase32[bytes[25]] | (_inverseBase32[bytes[24]] << 5)); + Unsafe.Add(ref ulidRef, 14) = (byte)((_inverseBase32[bytes[24]] >> 3) | (_inverseBase32[bytes[23]] << 2) | (_inverseBase32[bytes[22]] << 7)); + Unsafe.Add(ref ulidRef, 13) = (byte)((_inverseBase32[bytes[22]] >> 1) | (_inverseBase32[bytes[21]] << 4)); + Unsafe.Add(ref ulidRef, 12) = (byte)((_inverseBase32[bytes[21]] >> 4) | (_inverseBase32[bytes[20]] << 1) | (_inverseBase32[bytes[19]] << 6)); + Unsafe.Add(ref ulidRef, 11) = (byte)((_inverseBase32[bytes[19]] >> 2) | (_inverseBase32[bytes[18]] << 3)); + Unsafe.Add(ref ulidRef, 10) = (byte)(_inverseBase32[bytes[17]] | (_inverseBase32[bytes[16]] << 5)); + Unsafe.Add(ref ulidRef, 09) = (byte)((_inverseBase32[bytes[16]] >> 3) | (_inverseBase32[bytes[15]] << 2) | (_inverseBase32[bytes[14]] << 7)); + Unsafe.Add(ref ulidRef, 08) = (byte)((_inverseBase32[bytes[14]] >> 1) | (_inverseBase32[bytes[13]] << 4)); + Unsafe.Add(ref ulidRef, 07) = (byte)((_inverseBase32[bytes[13]] >> 4) | (_inverseBase32[bytes[12]] << 1) | (_inverseBase32[bytes[11]] << 6)); + Unsafe.Add(ref ulidRef, 06) = (byte)((_inverseBase32[bytes[11]] >> 2) | (_inverseBase32[bytes[10]] << 3)); + Unsafe.Add(ref ulidRef, 05) = (byte)(_inverseBase32[bytes[9]] | (_inverseBase32[bytes[8]] << 5)); + Unsafe.Add(ref ulidRef, 04) = (byte)((_inverseBase32[bytes[8]] >> 3) | (_inverseBase32[bytes[7]] << 2) | (_inverseBase32[bytes[6]] << 7)); + Unsafe.Add(ref ulidRef, 03) = (byte)((_inverseBase32[bytes[6]] >> 1) | (_inverseBase32[bytes[5]] << 4)); + Unsafe.Add(ref ulidRef, 02) = (byte)((_inverseBase32[bytes[5]] >> 4) | (_inverseBase32[bytes[4]] << 1) | (_inverseBase32[bytes[3]] << 6)); + Unsafe.Add(ref ulidRef, 01) = (byte)((_inverseBase32[bytes[3]] >> 2) | (_inverseBase32[bytes[2]] << 3)); + Unsafe.Add(ref ulidRef, 00) = (byte)(_inverseBase32[bytes[1]] | (_inverseBase32[bytes[0]] << 5)); return ulid; } @@ -359,37 +351,35 @@ private bool TryFill(Span span, T[] map) return false; } - // Eliminate bounds-check of span + // Encode randomness span[25] = map[_r9 & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][111|11111|] + span[24] = map[((_r8 & 0x3) << 3) | (_r9 >> 5)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][111111|11][111|11111] + span[23] = map[(_r8 >> 2) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][1|11111|11][11111111] + span[22] = map[((_r7 & 0xF) << 1) | (_r8 >> 7)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][1111|1111][1|1111111][11111111] + span[21] = map[((_r6 & 0x1) << 4) | (_r7 >> 4)]; // [11111111][11111111][11111111][11111111][11111111][11111111][1111111|1][1111|1111][11111111][11111111] + span[20] = map[(_r6 >> 1) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11|11111|1][11111111][11111111][11111111] + span[19] = map[((_r5 & 0x7) << 2) | (_r6 >> 6)]; // [11111111][11111111][11111111][11111111][11111111][11111|111][11|111111][11111111][11111111][11111111] + span[18] = map[(_r5 >> 3) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][|11111|111][11111111][11111111][11111111][11111111] + span[17] = map[_r4 & 0x1F]; // [11111111][11111111][11111111][11111111][111|11111|][11111111][11111111][11111111][11111111][11111111] + span[16] = map[((_r3 & 0x3) << 3) | (_r4 >> 5)]; // [11111111][11111111][11111111][11111111][111111|11][111|11111][11111111][11111111][11111111][11111111] + span[15] = map[(_r3 >> 2) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] + span[14] = map[((_r2 & 0xF) << 1) | (_r3 >> 7)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] + span[13] = map[((_r1 & 0x1) << 4) | (_r2 >> 4)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] + span[12] = map[(_r1 >> 1) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] + span[11] = map[((_r0 & 0x7) << 2) | (_r1 >> 6)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] + span[10] = map[(_r0 >> 3) & 0x1F]; // [|11111|111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] // Encode timestamp - span[0] = map[_t0 >> 5]; // |00[111|11111][11111111][11111111][11111111][11111111][11111111] - span[1] = map[_t0 & 0x1F]; // 00[111|11111|][11111111][11111111][11111111][11111111][11111111] - span[2] = map[_t1 >> 3]; // 00[11111111][|11111|111][11111111][11111111][11111111][11111111] - span[3] = map[((_t1 & 0x7) << 2) | (_t2 >> 6)]; // 00[11111111][11111|111][11|111111][11111111][11111111][11111111] - span[4] = map[(_t2 >> 1) & 0x1F]; // 00[11111111][11111111][11|11111|1][11111111][11111111][11111111] - span[5] = map[((_t2 & 0x1) << 4) | (_t3 >> 4)]; // 00[11111111][11111111][1111111|1][1111|1111][11111111][11111111] - span[6] = map[((_t3 & 0xF) << 1) | (_t4 >> 7)]; // 00[11111111][11111111][11111111][1111|1111][1|1111111][11111111] - span[7] = map[(_t4 >> 2) & 0x1F]; // 00[11111111][11111111][11111111][11111111][1|11111|11][11111111] - span[8] = map[((_t4 & 0x3) << 3) | (_t5 >> 5)]; // 00[11111111][11111111][11111111][11111111][111111|11][111|11111] span[9] = map[_t5 & 0x1F]; // 00[11111111][11111111][11111111][11111111][11111111][111|11111|] - - // Encode randomness - span[10] = map[(_r0 >> 3) & 0x1F]; // [|11111|111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] - span[11] = map[((_r0 & 0x7) << 2) | (_r1 >> 6)]; // [11111|111][11|111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] - span[12] = map[(_r1 >> 1) & 0x1F]; // [11111111][11|11111|1][11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] - span[13] = map[((_r1 & 0x1) << 4) | (_r2 >> 4)]; // [11111111][1111111|1][1111|1111][11111111][11111111][11111111][11111111][11111111][11111111][11111111] - span[14] = map[((_r2 & 0xF) << 1) | (_r3 >> 7)]; // [11111111][11111111][1111|1111][1|1111111][11111111][11111111][11111111][11111111][11111111][11111111] - span[15] = map[(_r3 >> 2) & 0x1F]; // [11111111][11111111][11111111][1|11111|11][11111111][11111111][11111111][11111111][11111111][11111111] - span[16] = map[((_r3 & 0x3) << 3) | (_r4 >> 5)]; // [11111111][11111111][11111111][111111|11][111|11111][11111111][11111111][11111111][11111111][11111111] - span[17] = map[_r4 & 0x1F]; // [11111111][11111111][11111111][11111111][111|11111|][11111111][11111111][11111111][11111111][11111111] - span[18] = map[(_r5 >> 3) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][|11111|111][11111111][11111111][11111111][11111111] - span[19] = map[((_r5 & 0x7) << 2) | (_r6 >> 6)]; // [11111111][11111111][11111111][11111111][11111111][11111|111][11|111111][11111111][11111111][11111111] - span[20] = map[(_r6 >> 1) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11|11111|1][11111111][11111111][11111111] - span[21] = map[((_r6 & 0x1) << 4) | (_r7 >> 4)]; // [11111111][11111111][11111111][11111111][11111111][11111111][1111111|1][1111|1111][11111111][11111111] - span[22] = map[((_r7 & 0xF) << 1) | (_r8 >> 7)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][1111|1111][1|1111111][11111111] - span[23] = map[(_r8 >> 2) & 0x1F]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][1|11111|11][11111111] - span[24] = map[((_r8 & 0x3) << 3) | (_r9 >> 5)]; // [11111111][11111111][11111111][11111111][11111111][11111111][11111111][11111111][111111|11][111|11111] + span[8] = map[((_t4 & 0x3) << 3) | (_t5 >> 5)]; // 00[11111111][11111111][11111111][11111111][111111|11][111|11111] + span[7] = map[(_t4 >> 2) & 0x1F]; // 00[11111111][11111111][11111111][11111111][1|11111|11][11111111] + span[6] = map[((_t3 & 0xF) << 1) | (_t4 >> 7)]; // 00[11111111][11111111][11111111][1111|1111][1|1111111][11111111] + span[5] = map[((_t2 & 0x1) << 4) | (_t3 >> 4)]; // 00[11111111][11111111][1111111|1][1111|1111][11111111][11111111] + span[4] = map[(_t2 >> 1) & 0x1F]; // 00[11111111][11111111][11|11111|1][11111111][11111111][11111111] + 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] return true; }