Skip to content

perf(cache): use a set of tags for wheel cache lookup - #14122

Merged
ichard26 merged 3 commits into
pypa:mainfrom
KRRT7:perf/cache-supported-tags-set
Jun 30, 2026
Merged

perf(cache): use a set of tags for wheel cache lookup#14122
ichard26 merged 3 commits into
pypa:mainfrom
KRRT7:perf/cache-supported-tags-set

Conversation

@KRRT7

@KRRT7 KRRT7 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Build a supported-tag set once per cached wheel lookup and reuse it for compatibility checks
  • Preserve the ordered supported-tag list for support_index_min() so cached wheel preference is unchanged
  • Avoid repeated frozenset.isdisjoint(list) scans when many cached wheels are considered for the same source link

Performance Model

Observed path Target path
Test each cached wheel against the same ordered supported-tag list Convert supported tags to a set once per cache lookup
Repeat broad list scans before ranking candidates Use set membership for compatibility, then use the ordered list only for priority
Spend cache lookup time hashing tags during isdisjoint(list) Reuse hashed supported tags across all candidate checks in the lookup

Hypothesis: repeated compatibility scans over the same supported-tag list are a meaningful share of SimpleWheelCache.get() runtime when a cache directory contains many wheels for one project.

Profile before the change confirmed this: in 20 cache lookups over 2,000 wheel candidates, Wheel.supported() / frozenset.isdisjoint(list) accounted for about 4.8s of 5.2s cumulative runtime, with 52.9M Tag.__hash__ calls.

After the change, the same profile no longer has Wheel.supported() in the top cumulative entries; wheel filename parsing is the remaining dominant cost.

Benchmark

Apple M3, 24 GiB RAM, CPython 3.14.5

Target workload: SimpleWheelCache.get() selecting a cached wheel for one source link from 2,000 same-project cached wheel filenames. 1,999 candidates are unsupported py2-none-any wheels and the final candidate is supported py3-none-any. This represents cached wheel selection where pip must filter many cached builds before returning the best compatible wheel.

Min Median Mean OPS Rounds
6b0011b49 (base) 70.383ms 71.929ms 71.840ms 13.9 ops/s 40
95191c130 (head) 4.184ms 4.265ms 4.278ms 233.7 ops/s 40
Speedup 16.82x 16.87x 16.79x 16.81x

I also ran python -m timeit in the working branch environment:

Result
Base 100 loops, best of 11: 72.2 msec per loop
Head 100 loops, best of 11: 4.58 msec per loop
Reproduce the benchmark locally
uv run python -c 'import statistics, time
from pip._internal.cache import SimpleWheelCache
from pip._internal.models.link import Link
from pip._internal.utils.compatibility_tags import get_supported

class BenchCache(SimpleWheelCache):
    def __init__(self, candidates):
        super().__init__("/tmp/pip-bench-cache")
        self._candidates = candidates
    def _get_candidates(self, link, canonical_package_name):
        return self._candidates

link = Link("https://example.invalid/package-1.0.tar.gz")
supported_tags = get_supported()
candidates = [(f"package-1.0-{i}-py2-none-any.whl", "/tmp/pip-bench-cache") for i in range(1999)]
candidates.append(("package-1.0-2000-py3-none-any.whl", "/tmp/pip-bench-cache"))
cache = BenchCache(candidates)
for _ in range(5):
    cache.get(link, "package", supported_tags)
samples = []
for _ in range(40):
    start = time.perf_counter()
    cache.get(link, "package", supported_tags)
    samples.append(time.perf_counter() - start)
print(f"min={min(samples)*1000:.3f}ms median={statistics.median(samples)*1000:.3f}ms mean={statistics.mean(samples)*1000:.3f}ms ops={1/statistics.mean(samples):.1f} rounds={len(samples)}")'

Changelog

Added news/14122.bugfix.rst.

Stack

Order PR Branch Merge after
1 Current PR perf/cache-supported-tags-set main

Existing open performance PRs were checked and are independent of this change: #14108, #14106, #14088, #14103, #14045, #14026, and #13860.

Test plan

  • Benchmarked base vs. head with the command above
  • Profiled before and after with cProfile
  • uv run ruff check src/pip/_internal/cache.py tests/unit/test_cache.py
  • uv run pytest tests/unit/test_cache.py -q
  • uv run pytest tests/unit/test_cache.py tests/unit/test_models_wheel.py -q

@ichard26 ichard26 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks!

Comment thread news/14122.bugfix.rst Outdated
KRRT7 and others added 2 commits June 30, 2026 17:37
@ichard26 ichard26 changed the title perf(cache): reuse supported tag set for wheel cache perf(cache): use a set of tags for wheel cache lookup Jun 30, 2026
@ichard26
ichard26 merged commit 045f54a into pypa:main Jun 30, 2026
37 checks passed
@KRRT7
KRRT7 deleted the perf/cache-supported-tags-set branch June 30, 2026 23:01
YakBizzarro pushed a commit to YakBizzarro/pip that referenced this pull request Jul 9, 2026
@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Jul 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants