diff --git a/benchmark/PerfBenchmark/BenchmarkConfig.cs b/benchmark/PerfBenchmark/BenchmarkConfig.cs index 5d461e2..bb44486 100644 --- a/benchmark/PerfBenchmark/BenchmarkConfig.cs +++ b/benchmark/PerfBenchmark/BenchmarkConfig.cs @@ -15,7 +15,7 @@ public BenchmarkConfig() var baseConfig = Job.ShortRun.WithIterationCount(1).WithWarmupCount(1); // Add(baseConfig.With(Runtime.Clr).With(Jit.RyuJit).With(Platform.X64)); - Add(baseConfig.With(Runtime.Core).With(Jit.RyuJit).With(Platform.X64)); + Add(baseConfig.With(Jit.RyuJit).With(Platform.X64)); Add(MarkdownExporter.GitHub); Add(CsvExporter.Default); diff --git a/benchmark/PerfBenchmark/PerfBenchmark.csproj b/benchmark/PerfBenchmark/PerfBenchmark.csproj index a5fb602..2ad0b96 100644 --- a/benchmark/PerfBenchmark/PerfBenchmark.csproj +++ b/benchmark/PerfBenchmark/PerfBenchmark.csproj @@ -1,13 +1,13 @@ - + Exe - netcoreapp3.1 + netcoreapp3.1;net7.0 false - + diff --git a/src/Ulid.Unity/Assets/Scripts/Ulid/Ulid.cs b/src/Ulid.Unity/Assets/Scripts/Ulid/Ulid.cs index bc30465..e862bac 100644 --- a/src/Ulid.Unity/Assets/Scripts/Ulid/Ulid.cs +++ b/src/Ulid.Unity/Assets/Scripts/Ulid/Ulid.cs @@ -1,10 +1,16 @@ using System.Buffers; +using System.Buffers.Binary; using System.ComponentModel; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Intrinsics.X86; +using System.Runtime.Intrinsics; +#endif namespace System // wa-o, System Namespace!? { @@ -15,10 +21,17 @@ namespace System // wa-o, System Namespace!? [StructLayout(LayoutKind.Explicit, Size = 16)] [DebuggerDisplay("{ToString(),nq}")] [TypeConverter(typeof(UlidTypeConverter))] -#if NETCOREAPP3_1 || NET5_0 +#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonConverter(typeof(Cysharp.Serialization.Json.UlidJsonConverter))] #endif - public partial struct Ulid : IEquatable, IComparable + public partial struct Ulid : IEquatable, IComparable, IComparable +#if NET6_0_OR_GREATER +, ISpanFormattable +#endif +#if NET7_0_OR_GREATER +, ISpanParsable +#endif + { // https://en.wikipedia.org/wiki/Base32 static readonly char[] Base32Text = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".ToCharArray(); @@ -74,6 +87,20 @@ public DateTimeOffset Time { get { + if (BitConverter.IsLittleEndian) + { + // |A|B|C|D|E|F|G|H|... -> |F|E|D|C|B|A|0|0| + + // Lower |A|B|C|D| -> |D|C|B|A| + // Upper |E|F| -> |F|E| + // Time |F|E| + |0|0|D|C|B|A| + var lower = Unsafe.As(ref Unsafe.AsRef(this.timestamp0)); + var upper = Unsafe.As(ref Unsafe.AsRef(this.timestamp4)); + var time = (long)BinaryPrimitives.ReverseEndianness(upper) + (((long)BinaryPrimitives.ReverseEndianness(lower)) << 16); + + return DateTimeOffset.FromUnixTimeMilliseconds(time); + } + Span buffer = stackalloc byte[8]; buffer[0] = timestamp5; buffer[1] = timestamp4; @@ -91,14 +118,14 @@ internal Ulid(long timestampMilliseconds, XorShift64 random) : this() { // Get memory in stack and copy to ulid(Little->Big reverse order). - ref var fisrtByte = ref Unsafe.As(ref timestampMilliseconds); - this.timestamp0 = Unsafe.Add(ref fisrtByte, 5); - this.timestamp1 = Unsafe.Add(ref fisrtByte, 4); - this.timestamp2 = Unsafe.Add(ref fisrtByte, 3); - this.timestamp3 = Unsafe.Add(ref fisrtByte, 2); - this.timestamp4 = Unsafe.Add(ref fisrtByte, 1); - this.timestamp5 = Unsafe.Add(ref fisrtByte, 0); - + ref var firstByte = ref Unsafe.As(ref timestampMilliseconds); + this.timestamp0 = Unsafe.Add(ref firstByte, 5); + this.timestamp1 = Unsafe.Add(ref firstByte, 4); + this.timestamp2 = Unsafe.Add(ref firstByte, 3); + this.timestamp3 = Unsafe.Add(ref firstByte, 2); + this.timestamp4 = Unsafe.Add(ref firstByte, 1); + this.timestamp5 = Unsafe.Add(ref firstByte, 0); + // Get first byte of randomness from Ulid Struct. Unsafe.WriteUnaligned(ref randomness0, random.Next()); // randomness0~7(but use 0~1 only) Unsafe.WriteUnaligned(ref randomness2, random.Next()); // randomness2~9 @@ -107,13 +134,14 @@ internal Ulid(long timestampMilliseconds, XorShift64 random) internal Ulid(long timestampMilliseconds, ReadOnlySpan randomness) : this() { - ref var fisrtByte = ref Unsafe.As(ref timestampMilliseconds); - this.timestamp0 = Unsafe.Add(ref fisrtByte, 5); - this.timestamp1 = Unsafe.Add(ref fisrtByte, 4); - this.timestamp2 = Unsafe.Add(ref fisrtByte, 3); - this.timestamp3 = Unsafe.Add(ref fisrtByte, 2); - this.timestamp4 = Unsafe.Add(ref fisrtByte, 1); - this.timestamp5 = Unsafe.Add(ref fisrtByte, 0); + // Get memory in stack and copy to ulid(Little->Big reverse order). + ref var firstByte = ref Unsafe.As(ref timestampMilliseconds); + this.timestamp0 = Unsafe.Add(ref firstByte, 5); + this.timestamp1 = Unsafe.Add(ref firstByte, 4); + this.timestamp2 = Unsafe.Add(ref firstByte, 3); + this.timestamp3 = Unsafe.Add(ref firstByte, 2); + this.timestamp4 = Unsafe.Add(ref firstByte, 1); + this.timestamp5 = Unsafe.Add(ref firstByte, 0); ref var src = ref MemoryMarshal.GetReference(randomness); // length = 10 randomness0 = randomness[0]; @@ -160,16 +188,39 @@ internal Ulid(ReadOnlySpan base32) // source: https://github.com/dotnet/runtime/blob/4f9ae42d861fcb4be2fcd5d3d55d5f227d30e723/src/libraries/System.Private.CoreLib/src/System/Guid.cs public Ulid(Guid guid) { +#if NET6_0_OR_GREATER + if (IsVector128Supported && BitConverter.IsLittleEndian) + { + var vector = Unsafe.As>(ref guid); + var shuffled = Shuffle(vector, Vector128.Create((byte)3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15)); + + this = Unsafe.As, Ulid>(ref shuffled); + return; + } +#endif Span buf = stackalloc byte[16]; - MemoryMarshal.Write(buf, ref guid); if (BitConverter.IsLittleEndian) { - byte tmp; - tmp = buf[0]; buf[0] = buf[3]; buf[3] = tmp; - tmp = buf[1]; buf[1] = buf[2]; buf[2] = tmp; - tmp = buf[4]; buf[4] = buf[5]; buf[5] = tmp; - tmp = buf[6]; buf[6] = buf[7]; buf[7] = tmp; + // |A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P| + // |D|C|B|A|... + // ...|F|E|H|G|... + // ...|I|J|K|L|M|N|O|P| + ref var ptr = ref Unsafe.As(ref guid); + var lower = BinaryPrimitives.ReverseEndianness(ptr); + MemoryMarshal.Write(buf, ref lower); + + ptr = ref Unsafe.Add(ref ptr, 1); + var upper = ((ptr & 0x00_FF_00_FF) << 8) | ((ptr & 0xFF_00_FF_00) >> 8); + MemoryMarshal.Write(buf.Slice(4), ref upper); + + ref var upperBytes = ref Unsafe.As(ref Unsafe.Add(ref ptr, 1)); + MemoryMarshal.Write(buf.Slice(8), ref upperBytes); } + else + { + MemoryMarshal.Write(buf, ref guid); + } + this = MemoryMarshal.Read(buf); } @@ -393,65 +444,111 @@ public bool TryWriteStringify(Span span) public override string ToString() { +#if NETCOREAPP2_1_OR_GREATER + return string.Create(26, this, (span, state) => + { + state.TryWriteStringify(span); + }); +#else Span span = stackalloc char[26]; TryWriteStringify(span); unsafe { return new string((char*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(span)), 0, 26); } +#endif } - // Comparable/Equatable - - public override unsafe int GetHashCode() +#if NET6_0_OR_GREATER + // + //ISpanFormattable + // +#nullable enable + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) { - // Simply XOR, same algorithm of Guid.GetHashCode - fixed (void* p = &this.timestamp0) + if (TryWriteStringify(destination)) + { + charsWritten = 26; + return true; + } + else { - var a = (int*)p; - return (*a) ^ *(a + 1) ^ *(a + 2) ^ *(a + 3); + charsWritten = 0; + return false; } } - public unsafe bool Equals(Ulid other) - { - // readonly struct can not use Unsafe.As... - fixed (byte* a = &this.timestamp0) - { - byte* b = &other.timestamp0; + public string ToString(string? format, IFormatProvider? formatProvider) => ToString(); +#nullable disable +#endif - { - var x = *(ulong*)a; - var y = *(ulong*)b; - if (x != y) return false; - } - { - var x = *(ulong*)(a + 8); - var y = *(ulong*)(b + 8); - if (x != y) return false; - } +#if NET7_0_OR_GREATER + // + // IParsable + // +#nullable enable + /// + public static Ulid Parse(string s, IFormatProvider? provider) => Parse(s); - return true; - } - } + /// + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out Ulid result) => TryParse(s, out result); - public override bool Equals(object obj) - { - return (obj is Ulid other) ? this.Equals(other) : false; - } + // + // ISpanParsable + // + + /// + public static Ulid Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); - public static bool operator ==(Ulid a, Ulid b) + /// + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out Ulid result) => TryParse(s, out result); +#nullable disable +#endif + + // Comparable/Equatable + + public override int GetHashCode() { - return a.Equals(b); + ref int rA = ref Unsafe.As(ref Unsafe.AsRef(in this)); + return rA ^ Unsafe.Add(ref rA, 1) ^ Unsafe.Add(ref rA, 2) ^ Unsafe.Add(ref rA, 3); } - public static bool operator !=(Ulid a, Ulid b) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool EqualsCore(in Ulid left, in Ulid right) { - return !a.Equals(b); +#if NET7_0_OR_GREATER + if (Vector128.IsHardwareAccelerated) + { + return Unsafe.As>(ref Unsafe.AsRef(in left)) == Unsafe.As>(ref Unsafe.AsRef(in right)); + } +#endif +#if NET6_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 cmp = Sse2.CompareEqual(vA, vB); + return Sse2.MoveMask(cmp) == 0xFFFF; + } +#endif + + ref var rA = ref Unsafe.As(ref Unsafe.AsRef(in left)); + ref var rB = ref Unsafe.As(ref Unsafe.AsRef(in right)); + + // Compare each element + return rA == rB && Unsafe.Add(ref rA, 1) == Unsafe.Add(ref rB, 1); } + public bool Equals(Ulid other) => EqualsCore(this, other); + + public override bool Equals(object obj) => (obj is Ulid other) && EqualsCore(this, other); + + public static bool operator ==(Ulid a, Ulid b) => EqualsCore(a, b); + + public static bool operator !=(Ulid a, Ulid b) => !EqualsCore(a, b); + [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetResult(byte me, byte them) => me < them ? -1 : 1; + private static int GetResult(byte me, byte them) => me < them ? -1 : 1; public int CompareTo(Ulid other) { @@ -476,6 +573,24 @@ public int CompareTo(Ulid other) return 0; } +#nullable enable + public int CompareTo(object? value) + { + if (value == null) + { + return 1; + } + + if (value is not Ulid ulid) + { + throw new ArgumentException("Object must be of type ULID.", nameof(value)); + } + + return this.CompareTo(ulid); + } + +#nullable disable + public static explicit operator Guid(Ulid _this) { return _this.ToGuid(); @@ -490,17 +605,70 @@ public static explicit operator Guid(Ulid _this) /// The converted Guid value public Guid ToGuid() { +#if NET6_0_OR_GREATER + if (IsVector128Supported && BitConverter.IsLittleEndian) + { + var vector = Unsafe.As>(ref this); + var shuffled = Shuffle(vector, Vector128.Create((byte)3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15)); + + return Unsafe.As, Guid>(ref shuffled); + } +#endif Span buf = stackalloc byte[16]; - MemoryMarshal.Write(buf, ref this); if (BitConverter.IsLittleEndian) { - byte tmp; - tmp = buf[0]; buf[0] = buf[3]; buf[3] = tmp; - tmp = buf[1]; buf[1] = buf[2]; buf[2] = tmp; - tmp = buf[4]; buf[4] = buf[5]; buf[5] = tmp; - tmp = buf[6]; buf[6] = buf[7]; buf[7] = tmp; + // |A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P| + // |D|C|B|A|... + // ...|F|E|H|G|... + // ...|I|J|K|L|M|N|O|P| + ref var ptr = ref Unsafe.As(ref this); + var lower = BinaryPrimitives.ReverseEndianness(ptr); + MemoryMarshal.Write(buf, ref lower); + + ptr = ref Unsafe.Add(ref ptr, 1); + var upper = ((ptr & 0x00_FF_00_FF) << 8) | ((ptr & 0xFF_00_FF_00) >> 8); + MemoryMarshal.Write(buf.Slice(4), ref upper); + + ref var upperBytes = ref Unsafe.As(ref Unsafe.Add(ref ptr, 1)); + MemoryMarshal.Write(buf.Slice(8), ref upperBytes); } + else + { + MemoryMarshal.Write(buf, ref this); + } + return MemoryMarshal.Read(buf); } - } + +#if NET6_0_OR_GREATER + private static bool IsVector128Supported + { + get + { +#if NET7_0_OR_GREATER + return Vector128.IsHardwareAccelerated; +#endif + return Sse3.IsSupported; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Shuffle(Vector128 value, Vector128 mask) + { + Debug.Assert(BitConverter.IsLittleEndian); + Debug.Assert(IsVector128Supported); + +#if NET7_0_OR_GREATER + if (Vector128.IsHardwareAccelerated) + { + return Vector128.Shuffle(value, mask); + } +#endif + if (Ssse3.IsSupported) + { + return Ssse3.Shuffle(value, mask); + } + throw new NotImplementedException(); + } +#endif + } } diff --git a/src/Ulid/Ulid.cs b/src/Ulid/Ulid.cs index bc30465..e862bac 100644 --- a/src/Ulid/Ulid.cs +++ b/src/Ulid/Ulid.cs @@ -1,10 +1,16 @@ using System.Buffers; +using System.Buffers.Binary; using System.ComponentModel; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Intrinsics.X86; +using System.Runtime.Intrinsics; +#endif namespace System // wa-o, System Namespace!? { @@ -15,10 +21,17 @@ namespace System // wa-o, System Namespace!? [StructLayout(LayoutKind.Explicit, Size = 16)] [DebuggerDisplay("{ToString(),nq}")] [TypeConverter(typeof(UlidTypeConverter))] -#if NETCOREAPP3_1 || NET5_0 +#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonConverter(typeof(Cysharp.Serialization.Json.UlidJsonConverter))] #endif - public partial struct Ulid : IEquatable, IComparable + public partial struct Ulid : IEquatable, IComparable, IComparable +#if NET6_0_OR_GREATER +, ISpanFormattable +#endif +#if NET7_0_OR_GREATER +, ISpanParsable +#endif + { // https://en.wikipedia.org/wiki/Base32 static readonly char[] Base32Text = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".ToCharArray(); @@ -74,6 +87,20 @@ public DateTimeOffset Time { get { + if (BitConverter.IsLittleEndian) + { + // |A|B|C|D|E|F|G|H|... -> |F|E|D|C|B|A|0|0| + + // Lower |A|B|C|D| -> |D|C|B|A| + // Upper |E|F| -> |F|E| + // Time |F|E| + |0|0|D|C|B|A| + var lower = Unsafe.As(ref Unsafe.AsRef(this.timestamp0)); + var upper = Unsafe.As(ref Unsafe.AsRef(this.timestamp4)); + var time = (long)BinaryPrimitives.ReverseEndianness(upper) + (((long)BinaryPrimitives.ReverseEndianness(lower)) << 16); + + return DateTimeOffset.FromUnixTimeMilliseconds(time); + } + Span buffer = stackalloc byte[8]; buffer[0] = timestamp5; buffer[1] = timestamp4; @@ -91,14 +118,14 @@ internal Ulid(long timestampMilliseconds, XorShift64 random) : this() { // Get memory in stack and copy to ulid(Little->Big reverse order). - ref var fisrtByte = ref Unsafe.As(ref timestampMilliseconds); - this.timestamp0 = Unsafe.Add(ref fisrtByte, 5); - this.timestamp1 = Unsafe.Add(ref fisrtByte, 4); - this.timestamp2 = Unsafe.Add(ref fisrtByte, 3); - this.timestamp3 = Unsafe.Add(ref fisrtByte, 2); - this.timestamp4 = Unsafe.Add(ref fisrtByte, 1); - this.timestamp5 = Unsafe.Add(ref fisrtByte, 0); - + ref var firstByte = ref Unsafe.As(ref timestampMilliseconds); + this.timestamp0 = Unsafe.Add(ref firstByte, 5); + this.timestamp1 = Unsafe.Add(ref firstByte, 4); + this.timestamp2 = Unsafe.Add(ref firstByte, 3); + this.timestamp3 = Unsafe.Add(ref firstByte, 2); + this.timestamp4 = Unsafe.Add(ref firstByte, 1); + this.timestamp5 = Unsafe.Add(ref firstByte, 0); + // Get first byte of randomness from Ulid Struct. Unsafe.WriteUnaligned(ref randomness0, random.Next()); // randomness0~7(but use 0~1 only) Unsafe.WriteUnaligned(ref randomness2, random.Next()); // randomness2~9 @@ -107,13 +134,14 @@ internal Ulid(long timestampMilliseconds, XorShift64 random) internal Ulid(long timestampMilliseconds, ReadOnlySpan randomness) : this() { - ref var fisrtByte = ref Unsafe.As(ref timestampMilliseconds); - this.timestamp0 = Unsafe.Add(ref fisrtByte, 5); - this.timestamp1 = Unsafe.Add(ref fisrtByte, 4); - this.timestamp2 = Unsafe.Add(ref fisrtByte, 3); - this.timestamp3 = Unsafe.Add(ref fisrtByte, 2); - this.timestamp4 = Unsafe.Add(ref fisrtByte, 1); - this.timestamp5 = Unsafe.Add(ref fisrtByte, 0); + // Get memory in stack and copy to ulid(Little->Big reverse order). + ref var firstByte = ref Unsafe.As(ref timestampMilliseconds); + this.timestamp0 = Unsafe.Add(ref firstByte, 5); + this.timestamp1 = Unsafe.Add(ref firstByte, 4); + this.timestamp2 = Unsafe.Add(ref firstByte, 3); + this.timestamp3 = Unsafe.Add(ref firstByte, 2); + this.timestamp4 = Unsafe.Add(ref firstByte, 1); + this.timestamp5 = Unsafe.Add(ref firstByte, 0); ref var src = ref MemoryMarshal.GetReference(randomness); // length = 10 randomness0 = randomness[0]; @@ -160,16 +188,39 @@ internal Ulid(ReadOnlySpan base32) // source: https://github.com/dotnet/runtime/blob/4f9ae42d861fcb4be2fcd5d3d55d5f227d30e723/src/libraries/System.Private.CoreLib/src/System/Guid.cs public Ulid(Guid guid) { +#if NET6_0_OR_GREATER + if (IsVector128Supported && BitConverter.IsLittleEndian) + { + var vector = Unsafe.As>(ref guid); + var shuffled = Shuffle(vector, Vector128.Create((byte)3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15)); + + this = Unsafe.As, Ulid>(ref shuffled); + return; + } +#endif Span buf = stackalloc byte[16]; - MemoryMarshal.Write(buf, ref guid); if (BitConverter.IsLittleEndian) { - byte tmp; - tmp = buf[0]; buf[0] = buf[3]; buf[3] = tmp; - tmp = buf[1]; buf[1] = buf[2]; buf[2] = tmp; - tmp = buf[4]; buf[4] = buf[5]; buf[5] = tmp; - tmp = buf[6]; buf[6] = buf[7]; buf[7] = tmp; + // |A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P| + // |D|C|B|A|... + // ...|F|E|H|G|... + // ...|I|J|K|L|M|N|O|P| + ref var ptr = ref Unsafe.As(ref guid); + var lower = BinaryPrimitives.ReverseEndianness(ptr); + MemoryMarshal.Write(buf, ref lower); + + ptr = ref Unsafe.Add(ref ptr, 1); + var upper = ((ptr & 0x00_FF_00_FF) << 8) | ((ptr & 0xFF_00_FF_00) >> 8); + MemoryMarshal.Write(buf.Slice(4), ref upper); + + ref var upperBytes = ref Unsafe.As(ref Unsafe.Add(ref ptr, 1)); + MemoryMarshal.Write(buf.Slice(8), ref upperBytes); } + else + { + MemoryMarshal.Write(buf, ref guid); + } + this = MemoryMarshal.Read(buf); } @@ -393,65 +444,111 @@ public bool TryWriteStringify(Span span) public override string ToString() { +#if NETCOREAPP2_1_OR_GREATER + return string.Create(26, this, (span, state) => + { + state.TryWriteStringify(span); + }); +#else Span span = stackalloc char[26]; TryWriteStringify(span); unsafe { return new string((char*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(span)), 0, 26); } +#endif } - // Comparable/Equatable - - public override unsafe int GetHashCode() +#if NET6_0_OR_GREATER + // + //ISpanFormattable + // +#nullable enable + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) { - // Simply XOR, same algorithm of Guid.GetHashCode - fixed (void* p = &this.timestamp0) + if (TryWriteStringify(destination)) + { + charsWritten = 26; + return true; + } + else { - var a = (int*)p; - return (*a) ^ *(a + 1) ^ *(a + 2) ^ *(a + 3); + charsWritten = 0; + return false; } } - public unsafe bool Equals(Ulid other) - { - // readonly struct can not use Unsafe.As... - fixed (byte* a = &this.timestamp0) - { - byte* b = &other.timestamp0; + public string ToString(string? format, IFormatProvider? formatProvider) => ToString(); +#nullable disable +#endif - { - var x = *(ulong*)a; - var y = *(ulong*)b; - if (x != y) return false; - } - { - var x = *(ulong*)(a + 8); - var y = *(ulong*)(b + 8); - if (x != y) return false; - } +#if NET7_0_OR_GREATER + // + // IParsable + // +#nullable enable + /// + public static Ulid Parse(string s, IFormatProvider? provider) => Parse(s); - return true; - } - } + /// + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out Ulid result) => TryParse(s, out result); - public override bool Equals(object obj) - { - return (obj is Ulid other) ? this.Equals(other) : false; - } + // + // ISpanParsable + // + + /// + public static Ulid Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); - public static bool operator ==(Ulid a, Ulid b) + /// + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out Ulid result) => TryParse(s, out result); +#nullable disable +#endif + + // Comparable/Equatable + + public override int GetHashCode() { - return a.Equals(b); + ref int rA = ref Unsafe.As(ref Unsafe.AsRef(in this)); + return rA ^ Unsafe.Add(ref rA, 1) ^ Unsafe.Add(ref rA, 2) ^ Unsafe.Add(ref rA, 3); } - public static bool operator !=(Ulid a, Ulid b) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool EqualsCore(in Ulid left, in Ulid right) { - return !a.Equals(b); +#if NET7_0_OR_GREATER + if (Vector128.IsHardwareAccelerated) + { + return Unsafe.As>(ref Unsafe.AsRef(in left)) == Unsafe.As>(ref Unsafe.AsRef(in right)); + } +#endif +#if NET6_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 cmp = Sse2.CompareEqual(vA, vB); + return Sse2.MoveMask(cmp) == 0xFFFF; + } +#endif + + ref var rA = ref Unsafe.As(ref Unsafe.AsRef(in left)); + ref var rB = ref Unsafe.As(ref Unsafe.AsRef(in right)); + + // Compare each element + return rA == rB && Unsafe.Add(ref rA, 1) == Unsafe.Add(ref rB, 1); } + public bool Equals(Ulid other) => EqualsCore(this, other); + + public override bool Equals(object obj) => (obj is Ulid other) && EqualsCore(this, other); + + public static bool operator ==(Ulid a, Ulid b) => EqualsCore(a, b); + + public static bool operator !=(Ulid a, Ulid b) => !EqualsCore(a, b); + [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetResult(byte me, byte them) => me < them ? -1 : 1; + private static int GetResult(byte me, byte them) => me < them ? -1 : 1; public int CompareTo(Ulid other) { @@ -476,6 +573,24 @@ public int CompareTo(Ulid other) return 0; } +#nullable enable + public int CompareTo(object? value) + { + if (value == null) + { + return 1; + } + + if (value is not Ulid ulid) + { + throw new ArgumentException("Object must be of type ULID.", nameof(value)); + } + + return this.CompareTo(ulid); + } + +#nullable disable + public static explicit operator Guid(Ulid _this) { return _this.ToGuid(); @@ -490,17 +605,70 @@ public static explicit operator Guid(Ulid _this) /// The converted Guid value public Guid ToGuid() { +#if NET6_0_OR_GREATER + if (IsVector128Supported && BitConverter.IsLittleEndian) + { + var vector = Unsafe.As>(ref this); + var shuffled = Shuffle(vector, Vector128.Create((byte)3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15)); + + return Unsafe.As, Guid>(ref shuffled); + } +#endif Span buf = stackalloc byte[16]; - MemoryMarshal.Write(buf, ref this); if (BitConverter.IsLittleEndian) { - byte tmp; - tmp = buf[0]; buf[0] = buf[3]; buf[3] = tmp; - tmp = buf[1]; buf[1] = buf[2]; buf[2] = tmp; - tmp = buf[4]; buf[4] = buf[5]; buf[5] = tmp; - tmp = buf[6]; buf[6] = buf[7]; buf[7] = tmp; + // |A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P| + // |D|C|B|A|... + // ...|F|E|H|G|... + // ...|I|J|K|L|M|N|O|P| + ref var ptr = ref Unsafe.As(ref this); + var lower = BinaryPrimitives.ReverseEndianness(ptr); + MemoryMarshal.Write(buf, ref lower); + + ptr = ref Unsafe.Add(ref ptr, 1); + var upper = ((ptr & 0x00_FF_00_FF) << 8) | ((ptr & 0xFF_00_FF_00) >> 8); + MemoryMarshal.Write(buf.Slice(4), ref upper); + + ref var upperBytes = ref Unsafe.As(ref Unsafe.Add(ref ptr, 1)); + MemoryMarshal.Write(buf.Slice(8), ref upperBytes); } + else + { + MemoryMarshal.Write(buf, ref this); + } + return MemoryMarshal.Read(buf); } - } + +#if NET6_0_OR_GREATER + private static bool IsVector128Supported + { + get + { +#if NET7_0_OR_GREATER + return Vector128.IsHardwareAccelerated; +#endif + return Sse3.IsSupported; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Shuffle(Vector128 value, Vector128 mask) + { + Debug.Assert(BitConverter.IsLittleEndian); + Debug.Assert(IsVector128Supported); + +#if NET7_0_OR_GREATER + if (Vector128.IsHardwareAccelerated) + { + return Vector128.Shuffle(value, mask); + } +#endif + if (Ssse3.IsSupported) + { + return Ssse3.Shuffle(value, mask); + } + throw new NotImplementedException(); + } +#endif + } } diff --git a/src/Ulid/Ulid.csproj b/src/Ulid/Ulid.csproj index d64564f..60fe1d6 100644 --- a/src/Ulid/Ulid.csproj +++ b/src/Ulid/Ulid.csproj @@ -1,13 +1,14 @@  - netstandard2.0;netcoreapp2.1;netcoreapp3.1;net5.0 + netstandard2.0;netcoreapp2.1;netcoreapp3.1;net5.0;net6.0;net7.0 true System true release.snk true - + Latest + Ulid Fast .NET Standard(C#) Implementation of ULID. diff --git a/src/Ulid/UlidJsonConverter.cs b/src/Ulid/UlidJsonConverter.cs index ab46276..3aa0d78 100644 --- a/src/Ulid/UlidJsonConverter.cs +++ b/src/Ulid/UlidJsonConverter.cs @@ -1,4 +1,4 @@ -#if NETCOREAPP3_1 || NET5_0 || SYSTEM_TEXT_JSON +#if NETCOREAPP3_1_OR_GREATER || SYSTEM_TEXT_JSON using System; using System.Buffers; diff --git a/tests/Ulid.Tests/Ulid.Tests.csproj b/tests/Ulid.Tests/Ulid.Tests.csproj index 925f72f..47a978a 100644 --- a/tests/Ulid.Tests/Ulid.Tests.csproj +++ b/tests/Ulid.Tests/Ulid.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + netcoreapp3.1;net7.0 false diff --git a/tests/Ulid.Tests/UlidTest.cs b/tests/Ulid.Tests/UlidTest.cs index 72c7d4b..e3a4a92 100644 --- a/tests/Ulid.Tests/UlidTest.cs +++ b/tests/Ulid.Tests/UlidTest.cs @@ -48,6 +48,14 @@ public void Compare_Time() times.Select(x => Ulid.NewUlid(x)).OrderBy(x => x).Select(x => x.Time).Should().BeEquivalentTo(times.OrderBy(x => x)); } + [Fact] + public void HashCode() + { + var ulid = Ulid.Parse("01ARZ3NDEKTSV4RRFFQ69G5FAV"); + + Assert.Equal(-1363483029, ulid.GetHashCode()); + } + [Fact] public void Parse() { @@ -57,6 +65,7 @@ public void Parse() Ulid.Parse(nulid.ToString()).ToByteArray().Should().BeEquivalentTo(nulid.ToByteArray()); } } + [Fact] public void Randomness() { @@ -78,6 +87,22 @@ public void GuidInterop() ulid2.Should().BeEquivalentTo(ulid, "a Ulid-Guid roundtrip should result in identical values"); } + [Fact] + public void UlidCompareTo() + { + var largeUlid = Ulid.MaxValue; + var smallUlid = Ulid.MinValue; + + largeUlid.CompareTo(smallUlid).Should().Be(1); + smallUlid.CompareTo(largeUlid).Should().Be(-1); + smallUlid.CompareTo(smallUlid).Should().Be(0); + + object smallObject = (object)smallUlid; + largeUlid.CompareTo(smallUlid).Should().Be(1); + largeUlid.CompareTo(null).Should().Be(1); + largeUlid.Invoking(u=> u.CompareTo("")).Should().Throw(); + } + [Fact] public void GuidComparison() { @@ -101,10 +126,70 @@ public void UlidParseRejectsInvalidStrings() } [Fact] - void UlidTryParseFailsForInvalidStrings() + public void UlidTryParseFailsForInvalidStrings() { Assert.False(Ulid.TryParse("1234", out _)); Assert.False(Ulid.TryParse(Guid.NewGuid().ToString(), out _)); } + +#if NET6_0_OR_GREATER + [Fact] + public void UlidTryFormatReturnsStringAndLength() + { + var asString = "01ARZ3NDEKTSV4RRFFQ69G5FAV"; + var ulid = Ulid.Parse(asString); + var destination = new char[26]; + var largeDestination = new char[27]; + + ulid.TryFormat(destination, out int length, default, null).Should().BeTrue(); + destination.Should().BeEquivalentTo(asString); + length.Should().Be(26); + + ulid.TryFormat(largeDestination, out int largeLength, default, null).Should().BeTrue(); + largeDestination.AsSpan().Slice(0,26).ToArray().Should().BeEquivalentTo(asString); + largeLength.Should().Be(26); + } + + [Fact] + public void UlidTryFormatReturnsFalseWhenInvalidDestination() + { + var asString = "01ARZ3NDEKTSV4RRFFQ69G5FAV"; + var ulid = Ulid.Parse(asString); + var formatted = new char[25]; + + ulid.TryFormat(formatted, out int length, default, null).Should().BeFalse(); + formatted.Should().BeEquivalentTo(new char[25]); + length.Should().Be(0); + } +#endif +#if NET7_0_OR_GREATER + + [Fact] + public void IParsable() + { + for (int i = 0; i < 100; i++) + { + var nulid = NUlid.Ulid.NewUlid(); + Ulid.Parse(nulid.ToString(),null).ToByteArray().Should().BeEquivalentTo(nulid.ToByteArray()); + + Ulid.TryParse(nulid.ToString(), null, out Ulid ulid).Should().BeTrue(); + ulid.ToByteArray().Should().BeEquivalentTo(nulid.ToByteArray()); + } + } + + [Fact] + public void ISpanParsable() + { + for (int i = 0; i < 100; i++) + { + var nulid = NUlid.Ulid.NewUlid(); + Ulid.Parse(nulid.ToString().AsSpan(), null).ToByteArray().Should().BeEquivalentTo(nulid.ToByteArray()); + + Ulid.TryParse(nulid.ToString().AsSpan(), null, out Ulid ulid).Should().BeTrue(); + ulid.ToByteArray().Should().BeEquivalentTo(nulid.ToByteArray()); + } + } +#endif + } }