diff --git a/README.md b/README.md index 9040e0f..fa74f35 100644 --- a/README.md +++ b/README.md @@ -438,81 +438,82 @@ Benchmark scenarios also include comparisons against `Guid`, where functionality The following benchmarks were performed: ``` -BenchmarkDotNet v0.15.8, Windows 10 (10.0.19044.7184/21H2/November2021Update) +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 -.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 +.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 Job=DefaultJob | Type | Method | Mean | Error | Gen0 | Allocated | |---------------- |------------------- |------------:|----------:|-------:|----------:| -| Generate | ByteAetherUlid | 41.5749 ns | 0.1226 ns | - | - | -| Generate | ByteAetherUlidR1Bp | 47.8382 ns | 0.1300 ns | - | - | -| Generate | ByteAetherUlidR4Bp | 51.8018 ns | 0.1633 ns | - | - | -| Generate | ByteAetherUlidR1Bc | 89.3543 ns | 0.3834 ns | - | - | -| Generate | ByteAetherUlidR4Bc | 97.0720 ns | 0.5817 ns | - | - | -| Generate | NetUlid *(1) | 161.8118 ns | 0.5833 ns | 0.0095 | 80 B | -| Generate | NUlid *(2) | 49.6472 ns | 0.0899 ns | - | - | - -| GenerateNonMono | ByteAetherUlid | 90.3967 ns | 0.2756 ns | - | - | -| GenerateNonMono | ByteAetherUlidP | 42.4365 ns | 0.1973 ns | - | - | -| GenerateNonMono | Ulid *(3,4) | 39.7697 ns | 0.1061 ns | - | - | -| GenerateNonMono | NUlid | 92.7844 ns | 0.2120 ns | - | - | -| GenerateNonMono | Guid *(5) | 48.5736 ns | 0.1559 ns | - | - | -| GenerateNonMono | GuidV7 *(3,5) | 78.4207 ns | 0.5717 ns | - | - | - -| FromByteArray | ByteAetherUlid | 0.2572 ns | 0.0033 ns | - | - | -| FromByteArray | NetUlid | 0.6415 ns | 0.0093 ns | - | - | -| FromByteArray | Ulid | 0.2726 ns | 0.0125 ns | - | - | -| FromByteArray | NUlid | 0.0325 ns | 0.0028 ns | - | - | -| FromByteArray | Guid | 0.0232 ns | 0.0036 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 | - | - | -| FromString | Ulid | 14.9363 ns | 0.0178 ns | - | - | -| FromString | NUlid | 47.7679 ns | 0.1855 ns | 0.0086 | 72 B | -| FromString | Guid | 20.6854 ns | 0.2167 ns | - | - | - -| ToByteArray | ByteAetherUlid | 4.3934 ns | 0.1295 ns | 0.0048 | 40 B | -| ToByteArray | AsByteSpan *(6) | 0.0000 ns | 0.0000 ns | - | - | -| ToByteArray | NetUlid | 10.7272 ns | 0.1604 ns | 0.0048 | 40 B | -| 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.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 | -| ToString | Ulid | 12.8387 ns | 0.2766 ns | 0.0095 | 80 B | -| ToString | NUlid | 30.1114 ns | 0.1880 ns | 0.0095 | 80 B | -| ToString | Guid | 9.0335 ns | 0.1220 ns | 0.0115 | 96 B | - -| CompareTo | ByteAetherUlid | 0.0024 ns | 0.0046 ns | - | - | -| CompareTo | NetUlid | 3.4035 ns | 0.0304 ns | - | - | -| CompareTo | Ulid | 0.0002 ns | 0.0005 ns | - | - | -| CompareTo | NUlid | 0.4194 ns | 0.0083 ns | - | - | - -| Equals | ByteAetherUlid | 0.0021 ns | 0.0023 ns | - | - | -| Equals | NetUlid | 1.0259 ns | 0.0086 ns | - | - | -| Equals | Ulid | 0.0047 ns | 0.0085 ns | - | - | -| Equals | NUlid | 0.0036 ns | 0.0037 ns | - | - | -| Equals | Guid | 0.0001 ns | 0.0004 ns | - | - | - -| GetHashCode | ByteAetherUlid | 0.0012 ns | 0.0024 ns | - | - | -| GetHashCode | NetUlid | 9.9326 ns | 0.0403 ns | - | - | -| GetHashCode | Ulid | 0.0000 ns | 0.0000 ns | - | - | -| GetHashCode | NUlid | 6.0461 ns | 0.0258 ns | - | - | -| GetHashCode | Guid | 0.0000 ns | 0.0000 ns | - | - | +| 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.7978 ns | 0.0060 ns | - | - | +| FromByteArray | NetUlid | 1.4778 ns | 0.0053 ns | - | - | +| FromByteArray | Ulid | 1.1777 ns | 0.0042 ns | - | - | +| FromByteArray | NUlid | 1.1467 ns | 0.0077 ns | - | - | +| FromByteArray | Guid | 1.0420 ns | 0.0055 ns | - | - | + +| FromGuid | ByteAetherUlid | 0.8428 ns | 0.0140 ns | - | - | +| FromGuid | NetUlid | 1.7816 ns | 0.0219 ns | - | - | +| FromGuid | Ulid | 2.0271 ns | 0.0274 ns | - | - | +| FromGuid | NUlid | 1.0718 ns | 0.0353 ns | - | - | + +| FromString | ByteAetherUlid | 15.2981 ns | 0.0964 ns | - | - | +| FromString | NetUlid | 28.0353 ns | 0.3071 ns | - | - | +| FromString | Ulid | 17.4969 ns | 0.0463 ns | - | - | +| FromString | NUlid | 55.3277 ns | 0.1974 ns | 0.0086 | 72 B | +| FromString | Guid | 22.0493 ns | 0.1599 ns | - | - | + +| ToByteArray | ByteAetherUlid | 4.6643 ns | 0.0933 ns | 0.0048 | 40 B | +| ToByteArray | AsByteSpan *(6) | 0.7740 ns | 0.0041 ns | - | - | +| ToByteArray | NetUlid | 9.5123 ns | 0.1098 ns | 0.0048 | 40 B | +| ToByteArray | Ulid | 4.6310 ns | 0.0918 ns | 0.0048 | 40 B | +| ToByteArray | NUlid | 8.6575 ns | 0.1394 ns | 0.0048 | 40 B | + +| ToGuid | ByteAetherUlid | 0.8021 ns | 0.0073 ns | - | - | +| ToGuid | NetUlid | 10.2952 ns | 0.0195 ns | - | - | +| ToGuid | Ulid | 1.2186 ns | 0.0057 ns | - | - | +| ToGuid | NUlid | 0.7696 ns | 0.0039 ns | - | - | + +| ToString | ByteAetherUlid | 21.584 ns | 0.2609 ns | 0.0095 | 80 B | +| ToString | NetUlid | 27.315 ns | 0.3311 ns | 0.0095 | 80 B | +| ToString | Ulid | 23.141 ns | 0.3722 ns | 0.0095 | 80 B | +| ToString | NUlid | 27.861 ns | 0.2217 ns | 0.0095 | 80 B | +| ToString | Guid | 9.113 ns | 0.1121 ns | 0.0115 | 96 B | + +| CompareTo | ByteAetherUlid | 1.2997 ns | 0.0105 ns | - | - | +| CompareTo | NetUlid | 4.6796 ns | 0.0222 ns | - | - | +| CompareTo | Ulid | 6.7590 ns | 0.0167 ns | - | - | +| CompareTo | NUlid | 9.0615 ns | 0.0564 ns | - | - | +| CompareTo | Guid | 4.7613 ns | 0.0244 ns | - | - | + +| Equals | ByteAetherUlid | 1.0668 ns | 0.0065 ns | - | - | +| Equals | NetUlid | 1.9436 ns | 0.0050 ns | - | - | +| Equals | Ulid | 1.0576 ns | 0.0055 ns | - | - | +| Equals | NUlid | 1.0485 ns | 0.0040 ns | - | - | +| Equals | Guid | 1.1008 ns | 0.0073 ns | - | - | + +| GetHashCode | ByteAetherUlid | 0.9074 ns | 0.0057 ns | - | - | +| GetHashCode | NetUlid | 8.8425 ns | 0.0366 ns | - | - | +| GetHashCode | Ulid | 0.9066 ns | 0.0051 ns | - | - | +| GetHashCode | NUlid | 6.9351 ns | 0.0229 ns | - | - | +| GetHashCode | Guid | 0.9417 ns | 0.0072 ns | - | - | ``` Existing competitive libraries exhibit various deviations from the official ULID specification or present drawbacks: diff --git a/src/ByteAether.Ulid.Benchmarks/Program.cs b/src/ByteAether.Ulid.Benchmarks/Program.cs index a39cc99..feb3ec1 100644 --- a/src/ByteAether.Ulid.Benchmarks/Program.cs +++ b/src/ByteAether.Ulid.Benchmarks/Program.cs @@ -1,4 +1,5 @@ -using BenchmarkDotNet.Attributes; +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Order; @@ -106,183 +107,303 @@ public class GenerateNonMono public class ToByteArray : BenchmarkBase { [Benchmark] - public byte[] ByteAetherUlid() => _byteAetherUlid1.ToByteArray(); + public byte[] ByteAetherUlid() => _byteAether0[GetNextIndex()].ToByteArray(); [Benchmark] - public byte[] NetUlid() => _netUlid1.ToByteArray(); + public System.ReadOnlySpan AsByteSpan() => _byteAether0[GetNextIndex()].AsByteSpan(); [Benchmark] - public byte[] Ulid() => _ulid1.ToByteArray(); + public byte[] NetUlid() => _netUlid0[GetNextIndex()].ToByteArray(); [Benchmark] - public byte[] NUlid() => _nulid1.ToByteArray(); + public byte[] Ulid() => _cysharp0[GetNextIndex()].ToByteArray(); + + [Benchmark] + public byte[] NUlid() => _nulid0[GetNextIndex()].ToByteArray(); } [MemoryDiagnoser] -public class FromByteArray +public class FromByteArray : BenchmarkBase { - private static readonly byte[] _baseUlidBytes = ByteAether.Ulid.Ulid.New().ToByteArray(); - [Benchmark] - public ByteAether.Ulid.Ulid ByteAetherUlid() => ByteAether.Ulid.Ulid.New(_baseUlidBytes); + public ByteAether.Ulid.Ulid ByteAetherUlid() => ByteAether.Ulid.Ulid.New(_bytes[GetNextIndex()]); [Benchmark] - public NetUlid.Ulid NetUlid() => new(_baseUlidBytes); + public NetUlid.Ulid NetUlid() => new(_bytes[GetNextIndex()]); [Benchmark] - public System.Ulid Ulid() => new(_baseUlidBytes); + public System.Ulid Ulid() => new(_bytes[GetNextIndex()]); [Benchmark] - public NUlid.Ulid NUlid() => new(_baseUlidBytes); + public NUlid.Ulid NUlid() => new(_bytes[GetNextIndex()]); [Benchmark] - public System.Guid Guid() => new(_baseUlidBytes); + public System.Guid Guid() => new(_bytes[GetNextIndex()]); } [MemoryDiagnoser] public class ToString : BenchmarkBase { [Benchmark] - public string ByteAetherUlid() => _byteAetherUlid1.ToString(); + public string ByteAetherUlid() => _byteAether0[GetNextIndex()].ToString(); [Benchmark] - public string NetUlid() => _netUlid1.ToString(); + public string NetUlid() => _netUlid0[GetNextIndex()].ToString(); [Benchmark] - public string Ulid() => _ulid1.ToString(); + public string Ulid() => _cysharp0[GetNextIndex()].ToString(); [Benchmark] - public string NUlid() => _nulid1.ToString(); + public string NUlid() => _nulid0[GetNextIndex()].ToString(); [Benchmark] - public string Guid() => _guid1.ToString(); + public string Guid() => _guid0[GetNextIndex()].ToString(); } [MemoryDiagnoser] -public class FromString +public class FromString : BenchmarkBase { - private static readonly ByteAether.Ulid.Ulid _ulid = ByteAether.Ulid.Ulid.New(); - private static readonly string _ulidString = _ulid.ToString(); - private static readonly string _guidString = new System.Guid(_ulid.ToByteArray()).ToString(); - [Benchmark] - public ByteAether.Ulid.Ulid ByteAetherUlid() => ByteAether.Ulid.Ulid.Parse(_ulidString); + public ByteAether.Ulid.Ulid ByteAetherUlid() => ByteAether.Ulid.Ulid.Parse(_base32[GetNextIndex()]); [Benchmark] - public NetUlid.Ulid NetUlid() => global::NetUlid.Ulid.Parse(_ulidString); + public NetUlid.Ulid NetUlid() => global::NetUlid.Ulid.Parse(_base32[GetNextIndex()]); [Benchmark] - public System.Ulid Ulid() => System.Ulid.Parse(_ulidString); + public System.Ulid Ulid() => System.Ulid.Parse(_base32[GetNextIndex()]); [Benchmark] - public NUlid.Ulid NUlid() => global::NUlid.Ulid.Parse(_ulidString); + public NUlid.Ulid NUlid() => global::NUlid.Ulid.Parse(_base32[GetNextIndex()]); [Benchmark] - public System.Guid Guid() => System.Guid.Parse(_guidString); + public System.Guid Guid() => System.Guid.Parse(_guidHex[GetNextIndex()]); } [MemoryDiagnoser] public class ToGuid : BenchmarkBase { [Benchmark] - public System.Guid ByteAetherUlid() => _byteAetherUlid1.ToGuid(); + public System.Guid ByteAetherUlid() => _byteAether0[GetNextIndex()].ToGuid(); [Benchmark] - public System.Guid NetUlid() => new(_netUlid1.ToByteArray()); + public System.Guid NetUlid() => new(_netUlid0[GetNextIndex()].ToByteArray()); [Benchmark] - public System.Guid Ulid() => _ulid1.ToGuid(); + public System.Guid Ulid() => _cysharp0[GetNextIndex()].ToGuid(); [Benchmark] - public System.Guid NUlid() => _nulid1.ToGuid(); + public System.Guid NUlid() => _nulid0[GetNextIndex()].ToGuid(); } [MemoryDiagnoser] -public class FromGuid +public class FromGuid : BenchmarkBase { - private static readonly System.Guid _ulidGuid = ByteAether.Ulid.Ulid.New().ToGuid(); - [Benchmark] - public ByteAether.Ulid.Ulid ByteAetherUlid() => ByteAether.Ulid.Ulid.New(_ulidGuid); + public ByteAether.Ulid.Ulid ByteAetherUlid() => ByteAether.Ulid.Ulid.New(_guid0[GetNextIndex()]); [Benchmark] - public NetUlid.Ulid NetUlid() => new(_ulidGuid.ToByteArray()); + public NetUlid.Ulid NetUlid() => new(_guid0[GetNextIndex()].ToByteArray()); [Benchmark] - public System.Ulid Ulid() => new(_ulidGuid); + public System.Ulid Ulid() => new(_guid0[GetNextIndex()]); [Benchmark] - public NUlid.Ulid NUlid() => new(_ulidGuid); + public NUlid.Ulid NUlid() => new(_guid0[GetNextIndex()]); } [MemoryDiagnoser] public class Equals : BenchmarkBase { [Benchmark] - public bool ByteAetherUlid() => _byteAetherUlid1.Equals(_byteAetherUlid2); + public bool ByteAetherUlid() + { + var idx = GetNextIndex(); + return _byteAether0[idx].Equals(_byteAether1[idx]); + } [Benchmark] - public bool NetUlid() => _netUlid1.Equals(_netUlid2); + public bool NetUlid() + { + var idx = GetNextIndex(); + return _netUlid0[idx].Equals(_netUlid1[idx]); + } [Benchmark] - public bool Ulid() => _ulid1.Equals(_ulid2); + public bool Ulid() + { + var idx = GetNextIndex(); + return _cysharp0[idx].Equals(_cysharp1[idx]); + } [Benchmark] - public bool NUlid() => _nulid1.Equals(_nulid2); + public bool NUlid() + { + var idx = GetNextIndex(); + return _nulid0[idx].Equals(_nulid1[idx]); + } [Benchmark] - public bool Guid() => _guid1.Equals(_guid2); + public bool Guid() + { + var idx = GetNextIndex(); + return _guid0[idx].Equals(_guid1[idx]); + } } [MemoryDiagnoser] public class CompareTo : BenchmarkBase { [Benchmark] - public int ByteAetherUlid() => _byteAetherUlid1.CompareTo(_byteAetherUlid2); + public int ByteAetherUlid() + { + var idx = GetNextIndex(); + return _byteAether0[idx].CompareTo(_byteAether1[idx]); + } [Benchmark] - public int NetUlid() => _netUlid1.CompareTo(_netUlid2); + public int NetUlid() + { + var idx = GetNextIndex(); + return _netUlid0[idx].CompareTo(_netUlid1[idx]); + } [Benchmark] - public int Ulid() => _ulid1.CompareTo(_ulid2); + public int Ulid() + { + var idx = GetNextIndex(); + return _cysharp0[idx].CompareTo(_cysharp1[idx]); + } + + [Benchmark] + public int NUlid() + { + var idx = GetNextIndex(); + return _nulid0[idx].CompareTo(_nulid1[idx]); + } [Benchmark] - public int NUlid() => _nulid1.CompareTo(_nulid2); + public int Guid() + { + var idx = GetNextIndex(); + return _guid0[idx].CompareTo(_guid1[idx]); + } } [MemoryDiagnoser] public class GetHashCode : BenchmarkBase { [Benchmark] - public int ByteAetherUlid() => _byteAetherUlid1.GetHashCode(); + public int ByteAetherUlid() => _byteAether0[GetNextIndex()].GetHashCode(); [Benchmark] - public int NetUlid() => _netUlid1.GetHashCode(); + public int NetUlid() => _netUlid0[GetNextIndex()].GetHashCode(); [Benchmark] - public int Ulid() => _ulid1.GetHashCode(); + public int Ulid() => _cysharp0[GetNextIndex()].GetHashCode(); [Benchmark] - public int NUlid() => _nulid1.GetHashCode(); + public int NUlid() => _nulid0[GetNextIndex()].GetHashCode(); [Benchmark] - public int Guid() => _guid1.GetHashCode(); + public int Guid() => _guid0[GetNextIndex()].GetHashCode(); } public abstract class BenchmarkBase { - protected static readonly ByteAether.Ulid.Ulid _byteAetherUlid1 = ByteAether.Ulid.Ulid.New(); - protected static readonly ByteAether.Ulid.Ulid _byteAetherUlid2 = ByteAether.Ulid.Ulid.New(); - - protected static readonly NetUlid.Ulid _netUlid1 = new(_byteAetherUlid1.ToByteArray()); - protected static readonly NetUlid.Ulid _netUlid2 = new(_byteAetherUlid2.ToByteArray()); - - protected static readonly System.Ulid _ulid1 = new(_byteAetherUlid1.ToByteArray()); - protected static readonly System.Ulid _ulid2 = new(_byteAetherUlid2.ToByteArray()); - - protected static readonly NUlid.Ulid _nulid1 = new(_byteAetherUlid1.ToByteArray()); - protected static readonly NUlid.Ulid _nulid2 = new(_byteAetherUlid2.ToByteArray()); - - protected static readonly System.Guid _guid1 = new(_byteAetherUlid1.ToByteArray()); - protected static readonly System.Guid _guid2 = new(_byteAetherUlid2.ToByteArray()); + protected const int _iterationSize = 1024; + private int _idx; + + protected ByteAether.Ulid.Ulid[] _byteAether0 = null!; + protected ByteAether.Ulid.Ulid[] _byteAether1 = null!; + + protected NetUlid.Ulid[] _netUlid0 = null!; + protected NetUlid.Ulid[] _netUlid1 = null!; + + protected System.Ulid[] _cysharp0 = null!; + protected System.Ulid[] _cysharp1 = null!; + + protected NUlid.Ulid[] _nulid0 = null!; + protected NUlid.Ulid[] _nulid1 = null!; + + protected System.Guid[] _guid0 = null!; + protected System.Guid[] _guid1 = null!; + + protected string[] _base32 = null!; + protected string[] _guidHex = null!; + protected byte[][] _bytes = null!; + + /// + /// Fetches the current index and increments/wraps it without branching. + /// Inlining guarantees zero benchmark method call overhead. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected int GetNextIndex() + { + var current = _idx; + _idx = (current + 1) & (_idx - 1); + return current; + } + + [GlobalSetup] + public void GlobalSetup() + { + _idx = 0; + + _byteAether0 = new ByteAether.Ulid.Ulid[_iterationSize]; + _byteAether1 = new ByteAether.Ulid.Ulid[_iterationSize]; + + _netUlid0 = new NetUlid.Ulid[_iterationSize]; + _netUlid1 = new NetUlid.Ulid[_iterationSize]; + + _cysharp0 = new System.Ulid[_iterationSize]; + _cysharp1 = new System.Ulid[_iterationSize]; + + _nulid0 = new NUlid.Ulid[_iterationSize]; + _nulid1 = new NUlid.Ulid[_iterationSize]; + + _guid0 = new System.Guid[_iterationSize]; + _guid1 = new System.Guid[_iterationSize]; + + _base32 = new string[_iterationSize]; + _guidHex = new string[_iterationSize]; + _bytes = new byte[_iterationSize][]; + + var rand = new System.Random(42); + var leftBuffer = new byte[16]; + var rightBuffer = new byte[16]; + + for (var i = 0; i < _iterationSize; i++) + { + rand.NextBytes(leftBuffer); + rand.NextBytes(rightBuffer); + + // Scenarios + switch (i % 4) + { + case 0: break; // Left and Right are completely random + case 1: rightBuffer = leftBuffer; break; // Same values + case 2: System.Array.Copy(leftBuffer, 0, rightBuffer, 0, 6); break; // The first 6 bytes are the same (same timestamp) + case 3: System.Array.Copy(leftBuffer, 0, rightBuffer, 0, 15); break; // The first 15 bytes are the same (+1 increment) + } + + _byteAether0[i] = ByteAether.Ulid.Ulid.New(leftBuffer); + _byteAether1[i] = ByteAether.Ulid.Ulid.New(rightBuffer); + + _netUlid0[i] = new(leftBuffer); + _netUlid1[i] = new(rightBuffer); + + _cysharp0[i] = new(leftBuffer); + _cysharp1[i] = new(rightBuffer); + + _nulid0[i] = new(leftBuffer); + _nulid1[i] = new(rightBuffer); + + _guid0[i] = new(leftBuffer); + _guid1[i] = new(rightBuffer); + + _base32[i] = _byteAether0[i].ToString(); + _guidHex[i] = _guid0[i].ToString(); + _bytes[i] = leftBuffer; + } + } } \ No newline at end of file diff --git a/src/ByteAether.Ulid/ByteAether.Ulid.csproj b/src/ByteAether.Ulid/ByteAether.Ulid.csproj index 5e5f6bb..bc1ddcd 100644 --- a/src/ByteAether.Ulid/ByteAether.Ulid.csproj +++ b/src/ByteAether.Ulid/ByteAether.Ulid.csproj @@ -18,10 +18,8 @@ true snupkg - Ulid - High-performance, spec-compliant ULIDs engineered to prevent index fragmentation and random-part overflow errors. - -Ideal for database primary keys and distributed systems, this library ensures lexicographical sortability and human-readable Base32 encoding. It features configurable protection against enumeration attacks, is fully Native AOT-ready, and offers zero-allocation interoperability with GUIDs, databases, and JSON. Optimized for efficient database writes and robust, timestamped identification in modern .NET. + ByteAether.Ulid - High-Performance ULID for .NET + A fast, spec-compliant ULID (Universally Unique Lexicographically Sortable Identifier) implementation for modern .NET. Optimized for database primary keys, Native AOT, zero-allocation, GUID interoperability, and high-performance serialization. Joonatan Uusväli ByteAether © $(Authors), $(Company). All rights reserved. @@ -30,7 +28,7 @@ Ideal for database primary keys and distributed systems, this library ensures le git ByteAether.Ulid - ulid;identifier;unique-id;unique-identifier;universal-identifier;lexicographically-sortable;lexicographical;id-generator;globally-unique;globally-unique-identifier;distributed-systems;database;performance;guid;uuid;sortable;human-readable;monotonic;primary-key;base32;cross-platform;aot-ready + ulid;id;uuid;guid;id-generator;unique-id;aot-ready;efcore;dotnet;csharp $(RepositoryUrl)/tree/v$(PackageVersion) See $(RepositoryUrl)/releases/tag/v$(PackageVersion) PACKAGE.md diff --git a/src/ByteAether.Ulid/Ulid.Comparable.cs b/src/ByteAether.Ulid/Ulid.Comparable.cs index edc381f..0df0b58 100644 --- a/src/ByteAether.Ulid/Ulid.Comparable.cs +++ b/src/ByteAether.Ulid/Ulid.Comparable.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; #if NET7_0_OR_GREATER using System.Numerics; #endif @@ -89,28 +90,26 @@ public int CompareTo(Ulid other) [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif private static int CompareToCore(in Ulid left, in Ulid right) - => left._t0 != right._t0 ? GetResult(left._t0, right._t0) - : left._t1 != right._t1 ? GetResult(left._t1, right._t1) - : left._t2 != right._t2 ? GetResult(left._t2, right._t2) - : left._t3 != right._t3 ? GetResult(left._t3, right._t3) - : left._t4 != right._t4 ? GetResult(left._t4, right._t4) - : left._t5 != right._t5 ? GetResult(left._t5, right._t5) - : left._r0 != right._r0 ? GetResult(left._r0, right._r0) - : left._r1 != right._r1 ? GetResult(left._r1, right._r1) - : left._r2 != right._r2 ? GetResult(left._r2, right._r2) - : left._r3 != right._r3 ? GetResult(left._r3, right._r3) - : left._r4 != right._r4 ? GetResult(left._r4, right._r4) - : left._r5 != right._r5 ? GetResult(left._r5, right._r5) - : left._r6 != right._r6 ? GetResult(left._r6, right._r6) - : left._r7 != right._r7 ? GetResult(left._r7, right._r7) - : left._r8 != right._r8 ? GetResult(left._r8, right._r8) - : left._r9 != right._r9 ? GetResult(left._r9, right._r9) - : 0; + { + ref var rA = ref Unsafe.As(ref Unsafe.AsRef(in left)); + ref var rB = ref Unsafe.As(ref Unsafe.AsRef(in right)); -#if NETCOREAPP3_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#endif - private static int GetResult(byte left, byte right) => left < right ? -1 : 1; + var a = BinaryPrimitives.ReverseEndianness(rA); + var b = BinaryPrimitives.ReverseEndianness(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)); + + if (a != b) + { + return a < b ? -1 : 1; + } + + return 0; + } } \ No newline at end of file diff --git a/src/ByteAether.Ulid/Ulid.Equatable.cs b/src/ByteAether.Ulid/Ulid.Equatable.cs index f1837ed..1be2aee 100644 --- a/src/ByteAether.Ulid/Ulid.Equatable.cs +++ b/src/ByteAether.Ulid/Ulid.Equatable.cs @@ -1,11 +1,11 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -#if NETCOREAPP -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; -#endif #if NET7_0_OR_GREATER using System.Numerics; +using System.Runtime.Intrinsics; +#elif NETCOREAPP3_0_OR_GREATER +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; #endif namespace ByteAether.Ulid; @@ -84,16 +84,15 @@ private static bool EqualsCore(in Ulid left, in Ulid right) #if NET7_0_OR_GREATER if (Vector128.IsHardwareAccelerated) { - var vA = Unsafe.As>(ref Unsafe.AsRef(in left)); - var vB = Unsafe.As>(ref Unsafe.AsRef(in right)); + var vA = Vector128.LoadUnsafe(ref Unsafe.As(ref Unsafe.AsRef(in left))); + var vB = Vector128.LoadUnsafe(ref Unsafe.As(ref Unsafe.AsRef(in right))); return vA == vB; } -#endif -#if NETCOREAPP +#elif NETCOREAPP3_0_OR_GREATER if (Sse2.IsSupported) { - var vA = Unsafe.As>(ref Unsafe.AsRef(in left)); - var vB = Unsafe.As>(ref Unsafe.AsRef(in right)); + var vA = Unsafe.ReadUnaligned>(ref Unsafe.As(ref Unsafe.AsRef(in left))); + var vB = Unsafe.ReadUnaligned>(ref Unsafe.As(ref Unsafe.AsRef(in right))); return Sse2.MoveMask(Sse2.CompareEqual(vA, vB)) == 0xFFFF; } #endif @@ -102,7 +101,7 @@ private static bool EqualsCore(in Ulid left, in Ulid right) ref var rB = ref Unsafe.As(ref Unsafe.AsRef(in right)); // XOR-compare instead of 2x 64bit long compare with AND - // Branchless XOR-compare is faster (1-3ns vs. 20-25ns) + // Branchless XOR-compare is faster (0.1787ns vs. 0.2463ns) var xor0 = rA ^ rB; var xor1 = Unsafe.Add(ref rA, 1) ^ Unsafe.Add(ref rB, 1); diff --git a/src/ByteAether.Ulid/Ulid.New.GenerationOptions.cs b/src/ByteAether.Ulid/Ulid.New.GenerationOptions.cs index f35bda2..fe244c8 100644 --- a/src/ByteAether.Ulid/Ulid.New.GenerationOptions.cs +++ b/src/ByteAether.Ulid/Ulid.New.GenerationOptions.cs @@ -95,7 +95,7 @@ public enum MonotonicityOptions /// - MonotonicIncrement: Guarantees monotonic ordering by incrementing /// the previous random by one if the same timestamp is generated consecutively.
/// - MonotonicRandom1Byte to MonotonicRandom4Byte: Ensures monotonicity by introducing a - /// randomized 1 to 4 bytes value as increment to previous Random part when timestamps are identical. + /// randomized 1 to 4 bytes value as an increment to the previous Random part when timestamps are identical. /// /// /// A value of the enum that specifies the monotonicity behavior. @@ -130,7 +130,7 @@ public MonotonicityOptions Monotonicity /// /// Specifies the random provider used to supply entropy for the Random component /// during monotonic increments when consecutive ULIDs share the same timestamp.
- /// It is utilized in maintaining monotonicity while ensuring random variation in ULID values. + /// It is used in maintaining monotonicity while ensuring random variation in ULID values. ///
/// /// An implementation of the interface that provides diff --git a/src/ByteAether.Ulid/Ulid.String.cs b/src/ByteAether.Ulid/Ulid.String.cs index f03f291..19c73d0 100644 --- a/src/ByteAether.Ulid/Ulid.String.cs +++ b/src/ByteAether.Ulid/Ulid.String.cs @@ -1,6 +1,8 @@ -using System.Diagnostics; +using System.Buffers.Binary; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; namespace ByteAether.Ulid; @@ -70,6 +72,16 @@ public readonly partial struct Ulid 22, 23, 24, 25, 26, // p-t 255, // u 27, 28, 29, 30, 31, // v-z + // Pad with value 255 so the array size is 256 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255 ]; /// @@ -120,37 +132,7 @@ public override string ToString() [MethodImpl(MethodImplOptions.AggressiveOptimization)] #endif public static Ulid Parse(ReadOnlySpan chars, IFormatProvider? provider = null) - { - // Sanity check. - if (chars.Length != UlidStringLength) - { - throw new FormatException("The input char sequence is not a valid ULID string representation."); - } - - // Decode. - Ulid ulid = default; - - 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; - } + => ParseCore(chars); /// /// Parses a ULID from a read-only span of bytes and returns the corresponding ULID value. @@ -166,36 +148,103 @@ public static Ulid Parse(ReadOnlySpan chars, IFormatProvider? provider = n [MethodImpl(MethodImplOptions.AggressiveOptimization)] #endif public static Ulid Parse(ReadOnlySpan bytes, IFormatProvider? provider = null) + => ParseCore(bytes); + +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif +#if NETCOREAPP3_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + private static unsafe Ulid ParseCore(ReadOnlySpan input) + where T : unmanaged { - // Sanity check. - if (bytes.Length != UlidStringLength) - { - throw new FormatException("The input byte sequence is not a valid ULID string representation."); - } + // Every T element is read as a byte. Other bits are ignored. + // We create 2 blocks of big-endian ulong values then reverse the endianness + // Creating a big-endian ulong and then reversing it is faster than creating directly a little-endian ulong + + if (input.Length != UlidStringLength) + { + throw new FormatException("The input sequence is not a valid ULID string representation."); + } + + var stepSize = sizeof(T); // We read the span as bytes and iterate by the size of an element + Ulid result = default; + + fixed (T* pSrc = &MemoryMarshal.GetReference(input)) + { + var pBytes = (byte*)pSrc; + ref var tableRef = ref _inverseBase32[0]; + ref var ulidRef = ref Unsafe.As(ref result); + + ulong t00 = Unsafe.Add(ref tableRef, pBytes[00 * stepSize]); + ulong t01 = Unsafe.Add(ref tableRef, pBytes[01 * stepSize]); + ulong t02 = Unsafe.Add(ref tableRef, pBytes[02 * stepSize]); + ulong t03 = Unsafe.Add(ref tableRef, pBytes[03 * stepSize]); + ulong t04 = Unsafe.Add(ref tableRef, pBytes[04 * stepSize]); + ulong t05 = Unsafe.Add(ref tableRef, pBytes[05 * stepSize]); + ulong t06 = Unsafe.Add(ref tableRef, pBytes[06 * stepSize]); + ulong t07 = Unsafe.Add(ref tableRef, pBytes[07 * stepSize]); + ulong t08 = Unsafe.Add(ref tableRef, pBytes[08 * stepSize]); + ulong t09 = Unsafe.Add(ref tableRef, pBytes[09 * stepSize]); + ulong r00 = Unsafe.Add(ref tableRef, pBytes[10 * stepSize]); + ulong r01 = Unsafe.Add(ref tableRef, pBytes[11 * stepSize]); + ulong r02 = Unsafe.Add(ref tableRef, pBytes[12 * stepSize]); + ulong r03 = Unsafe.Add(ref tableRef, pBytes[13 * stepSize]); + + var block1 = + (t00 << 61) + | (t01 << 56) + | (t02 << 51) + | (t03 << 46) + | (t04 << 41) + | (t05 << 36) + | (t06 << 31) + | (t07 << 26) + | (t08 << 21) + | (t09 << 16) + | (r00 << 11) + | (r01 << 6) + | (r02 << 1) + | (r03 >> 4); + + Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 0), BinaryPrimitives.ReverseEndianness(block1)); - // Decode. - Ulid ulid = default; + // Second block - ulong 64 bits + ulong r04 = Unsafe.Add(ref tableRef, pBytes[14 * stepSize]); + ulong r05 = Unsafe.Add(ref tableRef, pBytes[15 * stepSize]); + ulong r06 = Unsafe.Add(ref tableRef, pBytes[16 * stepSize]); + ulong r07 = Unsafe.Add(ref tableRef, pBytes[17 * stepSize]); + ulong r08 = Unsafe.Add(ref tableRef, pBytes[18 * stepSize]); + ulong r09 = Unsafe.Add(ref tableRef, pBytes[19 * stepSize]); + ulong r10 = Unsafe.Add(ref tableRef, pBytes[20 * stepSize]); + ulong r11 = Unsafe.Add(ref tableRef, pBytes[21 * stepSize]); + ulong r12 = Unsafe.Add(ref tableRef, pBytes[22 * stepSize]); + ulong r13 = Unsafe.Add(ref tableRef, pBytes[23 * stepSize]); + ulong r14 = Unsafe.Add(ref tableRef, pBytes[24 * stepSize]); + ulong r15 = Unsafe.Add(ref tableRef, pBytes[25 * stepSize]); - ref var ulidRef = ref Unsafe.As(ref ulid); + var block2 = + (r03 << 60) + | (r04 << 55) + | (r05 << 50) + | (r06 << 45) + | (r07 << 40) + | (r08 << 35) + | (r09 << 30) + | (r10 << 25) + | (r11 << 20) + | (r12 << 15) + | (r13 << 10) + | (r14 << 5) + | r15; - 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)); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref ulidRef, 8), BinaryPrimitives.ReverseEndianness(block2)); + } - return ulid; + return result; } ///