Skip to content

version: ReleaseKey, a release-group key that needs no render - #35

Merged
jonyoder merged 3 commits into
mainfrom
perf/release-key
Aug 14, 2026
Merged

version: ReleaseKey, a release-group key that needs no render#35
jonyoder merged 3 commits into
mainfrom
perf/release-key

Conversation

@jonyoder

Copy link
Copy Markdown
Collaborator

What

Adds version.ReleaseKey — a version's position in PEP 440's release order: its epoch and its release segments with trailing zeros stripped, and nothing else. Version.ReleaseKey() derives one, ReleaseKey.Compare orders two, ReleaseKey.String renders the smallest version string carrying a key.

It is deliberately coarser than the version order: 1.0, 1.0.0, 1.00, 1.0a1, 1.0.post3.dev2 and 1.0+ubuntu1 all occupy one position, because PEP 440 gives every one of them epoch 0 and, after stripping, the release tuple (1).

Why

Until now the only way to reach a version's release from outside this package was BaseVersion(), which renders the version back to text — a bytes.Buffer plus one math/big decimal conversion per segment — and left the caller splitting the result apart again.

go-pyresolver's PEP 440 set algebra did exactly that, once per candidate version per containment test. Measured at go-pyresolver v0.6.0, that derivation is 32.0% of a warm app-set resolution and 23.1% of wide-versions, with math/big.nat.itoa alone at 13.9%.

Deriving the key from the parsed fields instead:

BenchmarkReleaseKeyVsBaseVersionSplit/ReleaseKey-16          16.15 ns/op      0 B/op    0 allocs/op
BenchmarkReleaseKeyVsBaseVersionSplit/BaseVersionSplit-16   219.60 ns/op    192 B/op   10 allocs/op

13.6x faster, and the allocations go to zero — which is the point, since what the churn was buying was garbage collection. Comparing two keys is ~3 ns packed, ~8–11 ns on the general path.

How

Most keys pack into three words of six 32-bit segment fields, ordered by three integer comparisons. The rest — a nonzero epoch, more than six segments, or a segment at or above 2^32 — compare as arbitrary-precision integers. PEP 440 puts no ceiling on an epoch or a release segment (the grammar is [0-9]+) and datestamped and calendar versions push real segments high, so the general path is not optional.

packVersion's release-word assembly moves out into a shared packRelease, so the layout has one definition rather than two that could drift. That drift is the failure mode no single-encoding test can see: two copies would each order versions consistently within themselves while disagreeing with each other.

No allocation anywhere: the epoch is a by-value copy of a math/big header, and the release is a subslice of the Version's own — with its capacity clipped, so a later append by a holder of the key cannot write through into the Version's segments. That is the same aliasing hazard that made go-version's Parts.Padding a data race between two copies of one Version.

The zero value, which differs from Compare on purpose

The zero ReleaseKey compares equal to the key of "0" and "0.0", because those strip to an empty release too — where Version.Compare sorts an uninitialized Version strictly below every real version. That distinction lives in the full version order; the release grouping genuinely cannot see it. Both behaviours are asserted side by side in TestReleaseKeyZeroValue, so anyone who "fixes" one to match the other reads why first.

Testing

Check What holds it up
TestReleaseKeyConformanceFixture pypa/packaging 26.2's own release key (Version._key[0:2]), frozen into pypa-26.2-releasekey.ranked.gz. The only test holding ReleaseKey to the reference implementation directly rather than through Version's parse. Covers epochs and segments beyond a machine word, eight-segment releases, and leading zeros inside a segment — none of which the existing packed grid contains.
TestReleaseKeyMatchesBaseVersionSplit the render-and-split derivation this replaces, component by component over 348,752 corpus versions plus 980,000 ordering pairs. This is what says the migration changes no answer.
TestReleaseKeyPackedAgreesWithGeneral the two encodings against each other, pairwise, with a guard that fails if nothing in the sample packed (which would pass vacuously).
TestReleaseKeyRefinesVersionOrder the frozen full-version ranks: PEP 440's key is lexicographic with (epoch, release) first, so a strictly smaller release key must rank strictly below. Independent of the render-and-split reference.
TestPackReleaseIsTheOnlyReleaseLayout the refactor — every packable version's whole-version key must carry exactly packRelease's words.
TestReleaseKeyAllocatesNothing AllocsPerRun == 0 for both derivation and comparison, on five shapes including the unpackable ones.
TestReleaseKeyDoesNotAliasIntoTheVersion the capacity clip, by appending and checking the Version did not move.
Groups / ordering / total-order / suffix-ignoring hand-checked tables.

Each of the four load-bearing assertions was mutation-checked: removing the capacity clip, packing regardless of epoch, breaking the length tie-break in the general path, and under-stripping trailing zeros each turn the suite red, with the failure naming the defect.

go test ./... green, gofmt -l silent, golangci-lint run ./... (v2.11.2, from the module root) reports 0 issues.

Compatibility

Purely additive. No existing behaviour changes: the packVersion refactor is exact — the old strip loop's smallUint(seg, packedSegMax) == 0 test and the new big.Int.Sign() != 0 test agree on every input the grammar admits.

🤖 Generated with Claude Code

Getting at a version's release from outside this package meant
BaseVersion(), which renders it back to text -- a bytes.Buffer plus one
math/big decimal conversion per segment -- and left the caller splitting
the result apart again. go-pyresolver's PEP 440 set algebra did exactly
that once per candidate version per containment test, where it cost 32%
of a warm resolution.

ReleaseKey derives the same (epoch, trailing-zero-stripped release) from
the parsed fields instead: 16 ns and no allocations against 220 ns and 10
for the render-and-split, comparing in ~3 ns. Most keys pack into three
words of 32-bit segment fields; the rest compare as arbitrary-precision
integers, because PEP 440 puts no ceiling on an epoch or a segment and
datestamped releases push real ones high.

packVersion's release-word assembly moves into a shared packRelease, so
the layout has one definition rather than two that could drift.

The key is coarser than the version order on purpose -- 1.0, 1.00, 1.0a1
and 1.0+ubuntu1 are one position -- and coarser at the zero value, where
it folds the uninitialized Version in with "0" rather than sorting it
below. Both are asserted, the second alongside the Compare behaviour it
deliberately differs from.

Held to pypa/packaging 26.2 by a frozen fixture ranked on packaging's own
release key, to the render-and-split derivation it replaces across
348,752 corpus versions, and to the general path pairwise so the packed
fast path cannot hide a bug.
An unbriefed review seat found the doc claim misusable, and it was right.
String() renders the SHORTEST version string carrying a key, and the doc
called it the smallest one -- while the type doc sold itself for
"bracketing versions by release". Together those invite

	lo, _ := Parse(k.String())

as a group's lower bound, which silently drops every pre-release and dev
release of that release: 1.0a1 and 1.0.dev0 carry the key of "1" and sort
strictly BELOW Parse("1"). The group has no greatest member either, since
.postN and +local extend it upward without limit. Both facts are now
stated on the type, on String, in the changelog, and asserted by
TestReleaseKeyStringIsNotABound.

Also from that seat:

  - "group" reads as "map key", which a ReleaseKey cannot be. Say order
    or partition, and say why.
  - The "32% of a warm resolution" figure is not in the issue cited
    beside it, and a hard percentage in a public godoc rots. Replaced
    with the benchmark in this repository, which a reader can rerun.
  - The ~16 ns figure is the best case. The receiver is by value and the
    method does not inline, so a caller ranging over a []Version pays the
    struct copy too, ~24 ns. Documented rather than papered over; the
    value receiver stays, for consistency and because it cannot write
    through to the caller's Version.
  - The file explains why the per-segment loop uses a pointer conversion
    and then uses the value conversion twice elsewhere. Scoped the rule
    to the loop it is actually about.
  - packRelease now takes the already-stripped release, so its own strip
    finds nothing to redo.
  - TestReleaseKeyRefinesVersionOrder only asserted on strictly-below
    pairs, so it would have passed vacuously against a Compare that never
    returned one. It now counts them and fails at zero, like its
    neighbours.
@jonyoder
jonyoder merged commit 000d44c into main Aug 14, 2026
4 checks passed
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.

1 participant