Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,11 +434,11 @@ Benchmark scenarios also include comparisons against `Guid`, where functionality

The following benchmarks were performed:
```
BenchmarkDotNet v0.15.8, Windows 10 (10.0.19044.6809/21H2/November2021Update)
BenchmarkDotNet v0.15.8, Windows 10 (10.0.19044.7184/21H2/November2021Update)
AMD Ryzen 7 3700X 3.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK 10.0.200
[Host] : .NET 10.0.4 (10.0.4, 10.0.426.12010), X64 RyuJIT x86-64-v3
DefaultJob : .NET 10.0.4 (10.0.4, 10.0.426.12010), X64 RyuJIT x86-64-v3
.NET SDK 10.0.202
[Host] : .NET 10.0.6 (10.0.6, 10.0.626.17701), X64 RyuJIT x86-64-v3
DefaultJob : .NET 10.0.6 (10.0.6, 10.0.626.17701), X64 RyuJIT x86-64-v3

Job=DefaultJob

Expand All @@ -465,10 +465,10 @@ Job=DefaultJob
| FromByteArray | NUlid | 0.0325 ns | 0.0028 ns | - | - |
| FromByteArray | Guid | 0.0232 ns | 0.0036 ns | - | - |

| FromGuid | ByteAetherUlid | 0.0230 ns | 0.0028 ns | - | - |
| FromGuid | NetUlid | 1.2526 ns | 0.0124 ns | - | - |
| FromGuid | Ulid | 1.7307 ns | 0.0146 ns | - | - |
| FromGuid | NUlid | 0.5142 ns | 0.0096 ns | - | - |
| FromGuid | ByteAetherUlid | 0.0000 ns | 0.0000 ns | - | - |
| FromGuid | NetUlid | 1.2034 ns | 0.0798 ns | - | - |
| FromGuid | Ulid | 1.4057 ns | 0.0437 ns | - | - |
| FromGuid | NUlid | 0.1920 ns | 0.0089 ns | - | - |

| FromString | ByteAetherUlid | 13.6761 ns | 0.0780 ns | - | - |
| FromString | NetUlid | 27.1021 ns | 0.2000 ns | - | - |
Expand All @@ -482,10 +482,10 @@ Job=DefaultJob
| ToByteArray | Ulid | 4.1402 ns | 0.1311 ns | 0.0048 | 40 B |
| ToByteArray | NUlid | 4.5557 ns | 0.1312 ns | 0.0048 | 40 B |

| ToGuid | ByteAetherUlid | 0.0178 ns | 0.0045 ns | - | - |
| ToGuid | NetUlid | 10.4798 ns | 0.0226 ns | - | - |
| ToGuid | Ulid | 0.7470 ns | 0.0084 ns | - | - |
| ToGuid | NUlid | 0.1989 ns | 0.0028 ns | - | - |
| ToGuid | ByteAetherUlid | 0.0151 ns | 0.0031 ns | - | - |
| ToGuid | NetUlid | 9.9122 ns | 0.0858 ns | - | - |
| ToGuid | Ulid | 0.5244 ns | 0.0163 ns | - | - |
| ToGuid | NUlid | 0.1479 ns | 0.0042 ns | - | - |

| ToString | ByteAetherUlid | 12.4213 ns | 0.2890 ns | 0.0096 | 80 B |
| ToString | NetUlid | 24.3216 ns | 0.5226 ns | 0.0095 | 80 B |
Expand Down
12 changes: 6 additions & 6 deletions src/ByteAether.Ulid.Tests/Ulid.Boundaries.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public void EmptyUlid_ShouldBeDefault()

// Assert
Assert.Equal(default, ulid);
Assert.Equal(emptyBytes, ulid.AsByteSpan());
Assert.Equal(emptyBytes, ulid.ToByteArray());
}

[Fact]
Expand All @@ -22,7 +22,7 @@ public void MaxUlid_ShouldHaveAllBytesSetToMax()
var expected = Enumerable.Repeat((byte)0xFF, 16).ToArray();

// Assert
Assert.Equal(expected, ulid.AsByteSpan());
Assert.Equal(expected, ulid.ToByteArray());
}

[Fact]
Expand All @@ -36,7 +36,7 @@ public void MinAt_WithLongTimestamp_ShouldHaveZeroRandomComponent()

// Assert
// Last 10 bytes (random part) should be 0
Assert.All(ulid.AsByteSpan()[^10..].ToArray(), x => Assert.Equal(0, x));
Assert.All(ulid.AsByteSpan().Slice(6, 10).ToArray(), x => Assert.Equal(0, x));
Assert.Equal(timestamp, ulid.Time.ToUnixTimeMilliseconds());
}

Expand All @@ -51,7 +51,7 @@ public void MinAt_WithDateTimeOffset_ShouldHaveZeroRandomComponent()

// Assert
// Last 10 bytes (random part) should be 0
Assert.All(ulid.AsByteSpan()[^10..].ToArray(), x => Assert.Equal(0, x));
Assert.All(ulid.AsByteSpan().Slice(6, 10).ToArray(), x => Assert.Equal(0, x));
Assert.Equal(dto.ToUnixTimeMilliseconds(), ulid.Time.ToUnixTimeMilliseconds());
}

Expand All @@ -66,7 +66,7 @@ public void MaxAt_WithLongTimestamp_ShouldHaveMaxRandomComponent()

// Assert
// Last 10 bytes (random part) should be 0xFF
Assert.All(ulid.AsByteSpan()[^10..].ToArray(), x => Assert.Equal(0xFF, x));
Assert.All(ulid.AsByteSpan().Slice(6, 10).ToArray(), x => Assert.Equal(0xFF, x));
Assert.Equal(timestamp, ulid.Time.ToUnixTimeMilliseconds());
}

Expand All @@ -81,7 +81,7 @@ public void MaxAt_WithDateTimeOffset_ShouldHaveMaxRandomComponent()

// Assert
// Last 10 bytes (random part) should be 0xFF
Assert.All(ulid.AsByteSpan()[^10..].ToArray(), x => Assert.Equal(0xFF, x));
Assert.All(ulid.AsByteSpan().Slice(6, 10).ToArray(), x => Assert.Equal(0xFF, x));
Assert.Equal(dto.ToUnixTimeMilliseconds(), ulid.Time.ToUnixTimeMilliseconds());
}
}
136 changes: 59 additions & 77 deletions src/ByteAether.Ulid/Ulid.Guid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,52 +20,22 @@ public readonly partial struct Ulid
/// Creates a new ULID using the specified GUID.
/// </summary>
/// <param name="guid">The GUID to initialize the ULID with.</param>
// HACK: We assume the layout of a Guid is the following:
// Int32, Int16, Int16, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8
// source: https://github.com/dotnet/runtime/blob/5c4686f831d34c2c127e943d0f0d144793eeb0ad/src/libraries/System.Private.CoreLib/src/System/Guid.cs
#if NET5_0_OR_GREATER
[SkipLocalsInit]
#endif
#if NETCOREAPP3_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
public static Ulid New(Guid guid)
{
#if NET6_0_OR_GREATER
if (BitConverter.IsLittleEndian && _isVector128Supported)
{
var vector = Unsafe.As<Guid, Vector128<byte>>(ref guid);
var shuffled = Shuffle(vector);

return Unsafe.As<Vector128<byte>, Ulid>(ref shuffled);
}
#endif
Span<byte> ulidBytes = stackalloc byte[_ulidSize];

if (BitConverter.IsLittleEndian)
{
// |A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|
// |D|C|B|A|...
// ...|F|E|H|G|...
// ...|I|J|K|L|M|N|O|P|
ref var ptr = ref Unsafe.As<Guid, uint>(ref guid);
var lower = BinaryPrimitives.ReverseEndianness(ptr);
MemoryMarshal.Write(ulidBytes, ref lower);


ptr = ref Unsafe.Add(ref ptr, 1);
var upper = ((ptr & 0x00_FF_00_FF) << 8) | ((ptr & 0xFF_00_FF_00) >> 8);
MemoryMarshal.Write(ulidBytes[4..], ref upper);

ref var upperBytes = ref Unsafe.As<uint, ulong>(ref Unsafe.Add(ref ptr, 1));
MemoryMarshal.Write(ulidBytes[8..], ref upperBytes);
}
else
{
MemoryMarshal.Write(ulidBytes, ref guid);
return Shuffle<Guid, Ulid>(ref guid);
}

return MemoryMarshal.Read<Ulid>(ulidBytes);
return Unsafe.As<Guid, Ulid>(ref guid);
}

/// <summary>
Expand All @@ -76,43 +46,18 @@ public static Ulid New(Guid guid)
[SkipLocalsInit]
#endif
#if NETCOREAPP3_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
public Guid ToGuid()
{
#if NETCOREAPP
if (BitConverter.IsLittleEndian && _isVector128Supported)
{
var vector = Unsafe.As<Ulid, Vector128<byte>>(ref Unsafe.AsRef(in this));
var shuffled = Shuffle(vector);

return Unsafe.As<Vector128<byte>, Guid>(ref shuffled);
}
#endif
Span<byte> guidBytes = stackalloc byte[_ulidSize];
if (BitConverter.IsLittleEndian)
{
// |A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|
// |D|C|B|A|...
// ...|F|E|H|G|...
// ...|I|J|K|L|M|N|O|P|
ref var ptr = ref Unsafe.As<Ulid, uint>(ref Unsafe.AsRef(in this));
var lower = BinaryPrimitives.ReverseEndianness(ptr);
MemoryMarshal.Write(guidBytes, ref lower);

ptr = ref Unsafe.Add(ref ptr, 1);
var upper = ((ptr & 0x00_FF_00_FF) << 8) | ((ptr & 0xFF_00_FF_00) >> 8);
MemoryMarshal.Write(guidBytes[4..], ref upper);

ref var upperBytes = ref Unsafe.As<uint, ulong>(ref Unsafe.Add(ref ptr, 1));
MemoryMarshal.Write(guidBytes[8..], ref upperBytes);
}
else
{
MemoryMarshal.Write(guidBytes, ref Unsafe.AsRef(in this));
return Shuffle<Ulid, Guid>(ref Unsafe.AsRef(in this));
}

return MemoryMarshal.Read<Guid>(guidBytes);
return Unsafe.As<Ulid, Guid>(ref Unsafe.AsRef(in this));
}

/// <summary>
Expand Down Expand Up @@ -140,29 +85,66 @@ public Guid ToGuid()
public static implicit operator Ulid(Guid guid) => New(guid);

#if NETCOREAPP
private static readonly bool _isVector128Supported =
private static readonly Vector128<byte> _shuffleMask
= Vector128.Create((byte)3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15);

private static readonly bool _isAccelerated =
#if NET7_0_OR_GREATER
Vector128.IsHardwareAccelerated;
#else
Vector128.IsHardwareAccelerated ||
#endif
Ssse3.IsSupported;
#endif

private static readonly Vector128<byte> _shuffleMask
= Vector128.Create((byte)3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15);

// HACK: We assume the layout of a Guid is the following:
// Int32, Int16, Int16, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8
// Source: https://github.com/dotnet/runtime/blob/5c4686f831d34c2c127e943d0f0d144793eeb0ad/src/libraries/System.Private.CoreLib/src/System/Guid.cs
// More info: https://stackoverflow.com/questions/10190817/guid-byte-order-in-net/10191075#10191075
#if NET5_0_OR_GREATER
[SkipLocalsInit]
#endif
#if NETCOREAPP3_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
private static Vector128<byte> Shuffle(Vector128<byte> value)
private static TOut Shuffle<TIn, TOut>(ref TIn bytes)
{
return
#if NETCOREAPP
if (_isAccelerated)
{
var vector = Unsafe.As<TIn, Vector128<byte>>(ref bytes);

#if NET7_0_OR_GREATER
Vector128.IsHardwareAccelerated ? Vector128.Shuffle(value, _shuffleMask) :
if (Vector128.IsHardwareAccelerated)
{
vector = Vector128.Shuffle(vector, _shuffleMask);
return Unsafe.As<Vector128<byte>, TOut>(ref vector);
}
#endif
Ssse3.IsSupported ? Ssse3.Shuffle(value, _shuffleMask) :
throw new NotImplementedException();
}

vector = Ssse3.Shuffle(vector, _shuffleMask);
return Unsafe.As<Vector128<byte>, TOut>(ref vector);
}
#endif

// |A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|
// |D|C|B|A|...
// ...|F|E|H|G|...
// ...|I|J|K|L|M|N|O|P|
Span<byte> result = new byte[_ulidSize];

ref var ptr = ref Unsafe.As<TIn, uint>(ref bytes);
var lower = BinaryPrimitives.ReverseEndianness(ptr);

ptr = ref Unsafe.Add(ref ptr, 1);
var upper = ((ptr & 0x00_FF_00_FF) << 8) | ((ptr & 0xFF_00_FF_00) >> 8);

ref var upperBytes = ref Unsafe.As<uint, ulong>(ref Unsafe.Add(ref ptr, 1));

MemoryMarshal.Write(result, ref lower);
MemoryMarshal.Write(result[4..], ref upper);
MemoryMarshal.Write(result[8..], ref upperBytes);

return Unsafe.As<byte, TOut>(ref result.GetPinnableReference());
}
}
Loading