Skip to content

perf(specifiers): keep range caches across canonicalization - #1253

Merged
henryiii merged 2 commits into
pypa:mainfrom
henryiii:perf/specifierset-keep-caches
Jun 16, 2026
Merged

henryiii merged 2 commits into
pypa:mainfrom
henryiii:perf/specifierset-keep-caches

Conversation

@henryiii

Copy link
Copy Markdown
Contributor

🤖 AI text below 🤖

SpecifierSet._canonical_specs() reset _is_unsatisfiable and _ranges whenever it canonicalized, so calling filter()/is_unsatisfiable() (which compute the range intersection) followed by str()/hash()/== (which canonicalize) threw the expensive intersection away, and the next range-based call recomputed it.

Canonicalization is just dict.fromkeys(sorted(specs, key=str)) — a reorder plus exact-duplicate dedup. Both caches depend only on the set of constraints: each Specifier._to_ranges() is a pure function of that one spec, and intersect_ranges is mathematical set intersection (commutative, associative, idempotent), so reordering or deduplicating specs cannot change the result. _is_unsatisfiable derives from the ranges plus symmetric checks over the === specs. The reset in the prereleases setter stays, since prereleases genuinely affects satisfiability.

The existing asv suite never interleaves the two operation families, so it shows no change (and no regressions). A targeted microbenchmark of is_unsatisfiable()str()is_unsatisfiable() on an 8-spec set (Python 3.14, Apple M5 Pro): 67.4μs → 42.0μs per round (1.6×). The cost on main is bounded — one extra range computation per object lifetime — so this mostly matters for many short-lived sets, e.g. resolvers hashing or printing specifier sets they also evaluate.

Regression tests pin that the cache object survives canonicalization and that contains() results are identical before/after for duplicate, unsorted, and post-release-!= sets.

Part of #1239.

🤖 Generated with Claude Code

@henryiii

Copy link
Copy Markdown
Contributor Author

@notatallshaw I'll leave this one up to you, if it overlaps with your work, feel free to close.

Comment thread src/packaging/specifiers.py Outdated
Comment thread src/packaging/specifiers.py Outdated
@henryiii
henryiii marked this pull request as ready for review June 13, 2026 04:58
henryiii and others added 2 commits June 15, 2026 19:51
SpecifierSet._canonical_specs() deduplicated and sorted self._specs and
then reset both self._is_unsatisfiable and self._ranges. Canonicalization
is semantics-preserving: it only changes the order and removes exact
duplicates of the spec tuple, never the set of constraints.

Both caches depend only on the multiset-as-set of specifiers:

- _ranges is the intersection of every spec's individual ranges. Each
  spec's _to_ranges() depends only on that single spec, and
  intersect_ranges implements set intersection, which is commutative,
  associative, and idempotent. Reordering the fold or dropping a
  duplicate spec (A intersect A == A) cannot change the result.
- _is_unsatisfiable derives from _get_ranges() plus the arbitrary-equality
  and prerelease-only checks, all of which are order- and
  duplicate-insensitive.

Resetting the caches therefore discarded valid, expensive work: calling
contains() (which builds ranges) followed by str()/hash()/== forced the
next contains() to recompute the intersection. Leaving the caches intact
keeps them warm. The prereleases.setter reset is unrelated and left alone.

Assisted-by: ClaudeCode:claude-opus-4-8
@henryiii
henryiii force-pushed the perf/specifierset-keep-caches branch from 5975a98 to 4ab044e Compare June 15, 2026 23:51
@henryiii
henryiii merged commit e64c20e into pypa:main Jun 16, 2026
69 checks passed
@henryiii
henryiii deleted the perf/specifierset-keep-caches branch June 16, 2026 03:19
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.

2 participants