如何实现兼具极致性能与可测试性的跨平台GUID生成器?
高性能可测试的Sequential Guid生成方案问题
我需要通过单元测试验证一个静态类,确保它在物理机、虚拟机、容器等各类环境下正常工作,且同一环境并行多次运行时能生成唯一结果。当前的问题是这个类使用了DateTime.Now调用,若改为非静态类并使用TimeProvider结构体虽然能实现可测试性,但必须满足以下限制:
- 该类每秒被调用数万次,生产环境记录显示2分钟内每秒调用151964235次,采用静态类就是为了避免GC压力
- 解决方案性能不能低于当前实现,包括运行时、对象创建时间及GC压力
- 具备可移植性,能在Windows、Linux和Mac系统运行
- 欢迎任何进一步提升性能的思路
- 当前核心关注点是
DateTime.Now,UniqueId与Environment.CurrentManagedThreadId后续再考虑
实现代码
namespace My.Utils; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Security.Cryptography; using System.Threading; /// <summary> /// <see cref="SequentialGuid" /> generates instances of <see cref="Guid" /> that are in ascending order. /// </summary> /// <remarks> /// It is both unique and ordered. /// </remarks> public static class SequentialGuid { private const uint GuidVersion8 = 0x8000; private const uint GuidVersion7 = 0x7000; private const uint Variant = 0x80000000; private const uint VariantMask = 0xC0000000; private const uint SequenceRollover = 0x1000; private static readonly MapperAb EmptyMapperAb = new(); private static readonly MapperCd BaseMapperCd; private static readonly uint C; private static long _lastMilliseconds; private static uint _sequence; private static SpinLock _spinLock; static SequentialGuid() { _spinLock = new SpinLock(false); int rnd = RandomNumberGenerator.GetInt32(int.MaxValue); rnd ^= Environment.MachineName.GetHashCode(); BaseMapperCd.C = (uint)rnd; BaseMapperCd.C &= ~VariantMask; BaseMapperCd.C |= Variant; BaseMapperCd.D = (uint)(Environment.ProcessId ^ rnd); C = (uint)rnd; C &= ~VariantMask; C |= Variant; } public static Guid CreateVersion7() { return CreateVersion7(DateTimeOffset.UtcNow); } public static Guid CreateVersion7(in DateTimeOffset timestamp) { var mapperAb = EmptyMapperAb; GetTicksAndSequence(timestamp, ref mapperAb); mapperAb.B |= GuidVersion7; var d = (uint)RandomNumberGenerator.GetInt32(int.MaxValue); Vector128<byte> vec = Vector128.Create(mapperAb.A, mapperAb.B, C, d).AsByte(); if (BitConverter.IsLittleEndian) { Vector128<byte> result = Vector128.Shuffle(vec, Vector128.Create((byte)0, 1, 2, 3, 6, 7, 4, 5, 11, 10, 9, 8, 15, 14, 13, 12)); return Unsafe.As<Vector128<byte>, Guid>(ref result); } return Unsafe.As<Vector128<byte>, Guid>(ref vec); } public static Guid CreateVersion8() { return CreateVersion8(DateTimeOffset.UtcNow); } public static Guid CreateVersion8(in DateTimeOffset timestamp) { var mapperAb = EmptyMapperAb; GetTicksAndSequence(timestamp, ref mapperAb); mapperAb.B |= GuidVersion8; Vector128<byte> vec = Vector128.Create(mapperAb.A, mapperAb.B, BaseMapperCd.C, BaseMapperCd.D).AsByte(); if (BitConverter.IsLittleEndian) { Vector128<byte> result = Vector128.Shuffle(vec, Vector128.Create((byte)0, 1, 2, 3, 6, 7, 4, 5, 11, 10, 9, 8, 15, 14, 13, 12)); return Unsafe.As<Vector128<byte>, Guid>(ref result); } return Unsafe.As<Vector128<byte>, Guid>(ref vec); } private static void GetTicksAndSequence(in DateTimeOffset timestamp, ref MapperAb mapperAb) { mapperAb.Ticks = timestamp.ToUnixTimeMilliseconds(); ArgumentOutOfRangeException.ThrowIfNegative(mapperAb.Ticks, nameof(timestamp)); var lockTaken = false; _spinLock.Enter(ref lockTaken); if (mapperAb.Ticks > _lastMilliseconds) { _sequence = 0; _lastMilliseconds = mapperAb.Ticks; } else { if (_sequence == SequenceRollover) // rollover will happen, so we increase ticks { _sequence = 0; ++_lastMilliseconds; } mapperAb.Ticks = _lastMilliseconds; } uint b = _sequence++; if (lockTaken) { _spinLock.Exit(); } mapperAb.Ticks <<= 16; mapperAb.B |= b; } [StructLayout(LayoutKind.Explicit)] private struct MapperAb { [FieldOffset(0)] public long Ticks; [FieldOffset(0)] public uint B; [FieldOffset(sizeof(uint))] public uint A; } [StructLayout(LayoutKind.Explicit)] private struct MapperCd { [FieldOffset(0)] public uint D; [FieldOffset(0)] public ushort ProcessId; [FieldOffset(sizeof(ushort))] public byte ThreadId; [FieldOffset(sizeof(ushort) + sizeof(byte))] public byte TaskId; [FieldOffset(sizeof(uint))] public uint C; } }
基准测试结果
基准测试显示:自定义UUIDv7实现性能优于.NET9内置的Guid.CreateVersion7,自定义UUIDv8的性能比内置UUIDv7提升约一倍,且所有自定义实现的内存分配均为0。
基准测试代码
namespace Guid.Benchmarks; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using BenchmarkDotNetVisualizer; using JetBrains.Annotations; using Matching.Utils; // ReSharper disable once ClassCanBeSealed.Global [MaxColumn] [MemoryDiagnoser(false)] [MinColumn] [RichHtmlExporter("Benchmark of UUID Creation", ["Job"], ["Mean", "Allocated"], ["Mean", "Min", "Max", "Allocated"], dividerMode: RenderTableDividerMode.SeparateTables, htmlWrapMode: HtmlDocumentWrapMode.RichDataTables)] [ShortRunJob(RuntimeMoniker.Net90)] [UsedImplicitly] public class BenchmarkSequentialGuidCreation { [Benchmark] public Guid BenchmarUuidV4() => Guid.NewGuid(); // UUIDv4 [Benchmark(Baseline = true)] public Guid BenchmarkUuidV7() => Guid.CreateVersion7(); // UUIDv7 [Benchmark] public Guid BenchmarkUuidV7Custom() => SequentialGuid.CreateVersion7(); // UUIDv7 [Benchmark] public Guid BenchmarkUuidV8Custom() => SequentialGuid.CreateVersion8(); // UUIDv7 }
更新内容
- 研究了.NET9中的UUID v7实现,逻辑与自定义实现基本一致,但性能明显更低
- 研究了EF Core 8的实现,该实现未使用真实时间组件,而是以对象初始化时间作为所有ID生成的基础,在进程启停场景下可能存在重复风险,且无其他唯一性保障机制
- 已将代码更新至最终版本,支持单元测试(通过接收
DateTimeOffset参数的重载方法),遵循Guid.CreateVersion7的实现逻辑,同时新增CreateVersion8方法,虽存在可预测性,但性能提升一倍
内容的提问来源于stack exchange,提问作者Iaman Swtrse
相关产品推荐
相关产品推荐

