Skip to content

v6.1: zero-alloc deterministic generators + ergonomic helpers#23

Merged
buvinghausen merged 12 commits into
masterfrom
v6.1/perf-and-ergonomics
May 12, 2026
Merged

v6.1: zero-alloc deterministic generators + ergonomic helpers#23
buvinghausen merged 12 commits into
masterfrom
v6.1/perf-and-ergonomics

Conversation

@buvinghausen

Copy link
Copy Markdown
Owner

Summary

SemVer minor release. No breaking changes.

Perf

  • 🚀 GuidV5.Create(byte[]) and GuidV8Name.Create(byte[]) drop from ~176 B/call to 0 B/call on .NET 6+. Rewrites GuidNameBased.Create to use one-shot
    SHA1.HashData / SHA256.HashData (both .NET 5+) with a stackalloc/ArrayPool concat buffer.
  • [SkipLocalsInit] applied to GuidV4, GuidV7, GuidV8Time, GuidNameBased generator methods — skips redundant JIT zero-init of stackalloc
    buffers (every byte is explicitly written).
  • 🧹 GuidV7.NewGuid() no-arg inlines ToUnixTimeMilliseconds extraction, saves one DateTimeOffset struct copy.

Ergonomic features (additive, purely)

  • Guid.MaxValue — RFC 9562 §5.10 max UUID constant. Backed by a private static readonly cache.
  • Guid.TryToDateTime(out DateTime) — BCL-style Try-pattern variant of the existing ToDateTime().
  • Guid.ToDateTimeOffset() + Guid.TryToDateTimeOffset(out DateTimeOffset) — explicit-UTC variants.
  • Guid.IsSequentialGuid() — public predicate delegating to internal SequentialGuidByteOrder.TryDetect.

Benchmark deltas (BenchmarkDotNet on .NET 10)

Method Before (v6.0) After (v6.1)
GuidV5.Create(byte[]) 20-char name 176 B
GuidV5.Create(byte[]) 71-char name 176 B
GuidV8Name.Create(byte[]) 20-char name 176 B
GuidV8Name.Create(byte[]) 71-char name 176 B
GuidV5.Create(string) 48–96 B (UTF-8 buffer, caller-controlled, unchanged)
Guid.NewGuid baseline
GuidV4, GuidV7, GuidV7.NewSqlGuid, GuidV8Time, GuidV8Time.NewSqlGuid – (unchanged)

Every generation path is now 0 B allocated on .NET 6+ (for the byte[] overloads of v5/v8 name-based).

Test plan

  • dotnet build clean on all 5 production TFMs (net10.0, net9.0, net8.0, net462, netstandard2.0); 0 warnings under TreatWarningsAsErrors=true
  • dotnet test — all tests pass (~25,067 total, +64 new from GuidExtensionsTests across 4 test TFMs)
  • RFC 9562 §A.4 SHA-1 vector (2ed6657d-e927-568b-95e1-2665a8aea6a2 for (Dns, "www.example.com")) still passes byte-for-byte
  • NameBenchmarks shows Allocated: – for both (byte[]) overloads at both name lengths
  • AOT smoke test exercises the four new extensions (JIT pass verified locally; full AOT publish runs in CI)
  • CI green on windows-latest with .NET 8/9/10 + AOT publish step

Implementation notes

  • The [SuppressMessage("Security", "CA5350")] on GuidNameBased.Create is required because SHA1.HashData is flagged by CA5350 (SHA-1 is weak for crypto
    purposes). RFC 9562 §A.4 mandates SHA-1 for UUIDv5 — this is interop with the spec, not a security boundary. The suppression is scoped to the single method with
    a Justification.
  • The conditional Span<byte> lifetime pattern in GuidNameBased.Create (stackalloc byte[256] declared at outer scope, conditional assignment to slice or
    ArrayPool-rented array, try/finally for release) is sound under C# scope-based span safety rules.
  • The static Guid.MaxValue extension lives inside the existing extension(Guid id) block (instance-receiver block hosting a static member). Separating into
    its own extension(Guid) block triggers CA1708 (members differing only by case) — consolidation resolves it.

Suggested follow-ups (out of scope here)

  • Add IsSequentialGuidTrueForSqlOrderedV8 for symmetry with the v7 SQL-order test.
  • A Span<char> / UTF-8 overload of GuidV5.Create / GuidV8Name.Create to close out the last allocation in the string path.
  • Small ExtensionBenchmarks.cs to keep IsSequentialGuid / TryToDateTime perf honest as the detector grows.

References

Replaces IncrementalHash with one-shot SHA1.HashData / SHA256.HashData on
NET6+. Concat buffer is stackalloc'd up to 256B with ArrayPool fallback.
NET8+ writes the namespace directly in network byte order; NET6/7 falls back
to TryWriteBytes + in-place swap. netstandard2.0 and NETFRAMEWORK paths
unchanged. Result: GuidV5/GuidV8Name no longer allocate on .NET 6+.
Per RFC 9562 §5.10 (Max UUID). Naming matches BCL convention
(int.MaxValue / DateTime.MaxValue).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a v6.1 minor release focused on improving performance of deterministic (name-based) UUID generation on modern TFMs and adding new Guid helper APIs for timestamp extraction and sequential-guid detection.

Changes:

  • Reworks name-based UUID generation to use one-shot SHA hashing with stackalloc/ArrayPool buffering on modern TFMs, plus a new in-place GUID byte swap helper.
  • Adds [SkipLocalsInit] to several hot-path generator methods to avoid redundant stack zeroing.
  • Introduces new Guid helpers (Guid.MaxValue, TryToDateTime, ToDateTimeOffset, TryToDateTimeOffset, IsSequentialGuid) with tests, AOT smoke coverage, and README documentation.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test/SequentialGuid.Tests/GuidExtensionsTests.cs Adds unit tests covering the new Guid helper APIs and Guid.MaxValue.
test/SequentialGuid.AotSmokeTest/Program.cs Extends the AOT smoke test to exercise the new helpers and ensure they work under AOT.
src/SequentialGuid/README.md Updates highlights and adds usage examples for the new helper APIs.
src/SequentialGuid/GuidV8Time.cs Applies [SkipLocalsInit] to the internal timestamp-based generator hot path.
src/SequentialGuid/GuidV7.cs Optimizes NewGuid() to avoid an extra DateTimeOffset copy and applies [SkipLocalsInit] to the hot path.
src/SequentialGuid/GuidV4.cs Applies [SkipLocalsInit] to the v4 generation hot path.
src/SequentialGuid/GuidNameBased.cs Rewrites name-based hashing to use one-shot SHA hashing with stack/pooled concatenation buffers on modern TFMs.
src/SequentialGuid/Extensions/GuidExtensions.cs Adds Guid.MaxValue and new timestamp/predicate helper methods.
src/SequentialGuid/Extensions/ByteArrayExtensions.cs Adds SwapGuidBytesInPlace() for efficient mixed-endian ↔ network-endian conversion.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/SequentialGuid/GuidNameBased.cs Outdated
Comment thread src/SequentialGuid/Extensions/GuidExtensions.cs Outdated
Comment thread src/SequentialGuid/README.md
Comment thread src/SequentialGuid/README.md Outdated
@buvinghausen

Copy link
Copy Markdown
Owner Author

@copilot apply changes based on the comments in this thread

Copilot AI and others added 2 commits May 12, 2026 18:44

Copilot AI commented May 12, 2026

Copy link
Copy Markdown
Contributor

@copilot apply changes based on the comments in this thread

Implemented the requested review-thread fixes in commits 0784be9 and c501f35: added checked length arithmetic in GuidNameBased, fixed the IsSequentialGuid XML summary, and corrected README framework/allocation wording plus the sample variable redeclaration issue.

@buvinghausen buvinghausen merged commit 6982797 into master May 12, 2026
1 check passed
@buvinghausen buvinghausen deleted the v6.1/perf-and-ergonomics branch May 12, 2026 19:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants