Repository navigation
Speed up ULID parsing, generation, and encoding; fix related correctness bugs - #104
Merged
Merged
Conversation
GetNextIndex only alternated between the first two entries, and all buffers were shared, so most compared pairs were identical.
Spans created from untracked pointers could outlive a GC move of the underlying object. netstandard2.0 AsByteSpan now copies.
Seramis
force-pushed
the
performance_optimizations
branch
from
October 10, 2026 12:21
8739214 to
8ecf7ef
Compare
Seramis
marked this pull request as ready for review
October 10, 2026 12:23
Shorter spans read past their end. Take ReadOnlySpan<byte> as documented.
Parse accepted invalid, non-ASCII and overflowing input. TryParse no longer relies on exceptions. Characters are decoded and accumulated one by one, which keeps validated parsing close to the previous cost.
Also define the SIMD shuffle mask once.
Lets tiered PGO optimize hot paths and removes per-TFM #if blocks.
Compute the current timestamp with unsigned division, and read a ULID's timestamp with a single 64-bit load instead of 6 byte loads.
Spinners no longer steal the lock's cache line with CAS, and the state's cache lines are padded against neighboring objects.
with-expressions previously shared the lock and last ULID; copies now get their own state. Monotonicity is validated with a range check instead of reflection, tested against every defined option, and the random increment mask is precomputed when it is set.
Removes byte swaps from the same-millisecond increment path.
Encode from two 64-bit loads instead of 16 byte loads, and store the inverse Base32 table as RVA data.
It only checked for a 16-byte length, unlike Parse/TryParse on the same type. Document the validation rules on IsValid.
Seramis
force-pushed
the
performance_optimizations
branch
from
October 10, 2026 12:38
8ecf7ef to
ed5370a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Performance-focused pass over the core
Ulidtype's hot paths (parsing, string encoding/decoding, generation, timestamp arithmetic, locking), bundled with several correctness fixes surfaced along the way.README.mdupdated with latest benchmark results.Performance:
MinAt/MaxAt/MaxValueare built without static byte arrays.AggressiveOptimizationletting tiered PGO optimize hot paths and removing per-TFM#ifblocks. MovedSkipLocalsInitto module-level.GenerationOptionscopies now get independent lock/state instead of sharing it with the source. Monotonicity is validated via range check (More AOT friendly), and the random increment mask is precomputed when set.Correctness fixes:
Parseaccepted invalid, non-ASCII, and overflowing input, andTryParserelied on exceptions internally. Both are fixed, with characters decoded and validated one by one.IsValid(ReadOnlySpan<byte>)only checked length (16 bytes) and didn't validate UTF-8 content likeParse/TryParsedo. Validation rules are now documented.New(timestamp, random)didn't validate the random span length, allowing reads past the end for shorter spans.AsByteSpanand a lock scope where spans from untracked pointers could outlive a GC move.netstandard2.0'sAsByteSpannow copies.UlidJsonConverterdidn't support escaped JSON strings.netstandard2.0RNG shims didn't return rented arrays.GetNextIndexonly alternated between the first two entries and all buffers were shared, so most compared pairs were identical.Type of Change
Checklist
main)..editorconfig)Additional Notes
No public API breaking changes.
This is primarily an internal hot-path rewrite plus validation fixes that tighten previously-loose input handling (
Parse,TryParse,IsValid,New(timestamp, random)).