GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — Revert normalize() performance changes (gh-143658) by jaraco · Pull Request #541 · python/importlib_metadata Conversation
|
|
Here's the performance diff I see locally: |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
e072b29 to
89e9eb7
Compare
7c0d919 to
0f88c48
Compare
Reverts the importlib_metadata/__init__.py normalize() changes introduced by the merge 8c5d91b (str.translate/replace optimizations from 3e7f3ac and 001db0d), restoring the original re.sub-based implementation. Closes #540 Co-Authored-By: Claude Opus 4.8 <[email protected]>
89e9eb7 to
73efee2
Compare
|
Looking at the Ubuntu 3.14 run, I see something similar: So the performance differences are real, but still in the nsec range (sub micro-second benefit). Let's see if we can quantify the value proposition, specifically by estimating the number of times this call is made globally. If we can't identify a real value proposition, the simpler implementation should prevail. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Following up on the value-proposition question, here's what I can piece together about how often 1. What the upstream change was justified onThe CPython PRs justify the change with a throughput micro-benchmark, not a workload: python/cpython#143660 normalizes all 8,344,947 PyPI names in 5.15 s → 1.38 s (3.7×); #144083 just compares the three implementations across Python versions. The rationale was delegated to the blog — but the blog's real-world numbers are about 2. Where
|
| Cold operation | normalize calls |
|---|---|
list(distributions()) |
95 (≈ N) |
version('pip') / metadata('pip') |
96 (≈ N+1) |
entry_points() |
190 (≈ 2N) |
3× version() in one process |
100 (index reused) |
The structural point: even a single version() lookup normalizes all installed names. Cost is O(N in the environment) per process, so normalize_perf (one call) understates per-process cost by ~N×.
Cleanly (locally bound, 4 representative names, Apple Silicon / 3.14): re.sub 529 ns → .lower().replace() 121 ns, Δ ≈ 414 ns/call (4.4×). Consistent with my earlier ~240–310 ns here and CPython's ~450 ns.
Cold operation wall-time vs. the normalize budget inside it (same machine, N=94):
| Operation | cold wall time | normalize budget |
share |
|---|---|---|---|
list(distributions()) |
1.09 ms | ~11 µs | ~1% |
entry_points() |
3.28 ms | ~23 µs | ~0.7% |
version('pip') |
6.70 ms | ~11 µs | ~0.2% |
The saving (Δ × N) is ~2–4% of discovery at most, <1% of entry_points/version. The rest is listdir/stat on site-packages and reading entry_points.txt/METADATA.
This matches the exercises.py suite: on the revert, the only benchmark that moved was normalize_perf itself (332 ns / 420 ns in my two runs). discovery (~173 µs), cached/uncached distribution (~169/273 µs), and entry_points() (~2.3 ms) did not register a change — the micro-op disappears into filesystem noise.
I instrumented Prepared.normalize and ran real pip commands (pip uses the importlib backend here):
| pip invocation | normalize calls |
|---|---|
pip list |
94 |
pip freeze |
94 |
pip install --dry-run … pytest |
94 |
pip show pip |
0 |
pip list --outdated --no-index |
0 |
So pip pays ~N calls once per invocation that enumerates the environment (via importlib.metadata.distributions()), and zero in its resolution/name-matching hot loop — that runs on packaging.canonicalize_name (92 call sites in pip/_internal), a different function that was optimized separately (pypa/packaging#1030). The "large resolution" scenario the blog uses to motivate the change does not exercise importlib.metadata.normalize at all. For pip, this is a flat ~94 × 414 ns ≈ 40 µs, once, inside a multi-hundred-ms process.
Anchoring on real data instead of guessing: normalizing all 8.3 M PyPI names saves ~3.78 s (the CPython benchmark delta). So you'd have to normalize the entirety of PyPI ~960 times to recoup one CPU-hour.
If I push a global figure anyway:
importlib.metadata pays ~1–2 N ≈ 150 calls (measured);entry_points()/version());→ ~5×10¹²–5×10¹³ calls/yr → at 414 ns, ~24 to ~240 CPU-days per year spread across all Python usage on Earth. Real, but per-invocation it's microseconds, inside operations already costing milliseconds of I/O.
This is a genuine 4.4× speedup of a function that isn't on any measurable hot path. Its cost is O(N) per process, yet still 1–4% of the I/O-bound operation containing it and invisible in every operation-level benchmark; even pip's dependency resolution doesn't route through it. The aggregate global saving is plausibly tens of CPU-days/year under generous assumptions — genuine but negligible per user, and unmeasurable against filesystem cost. Absent a concrete hot-path use case (which the blog, the CPython issue, and the reviews did not produce), I lean toward the simpler re.sub for readability/maintainability, keeping the performance and unit tests so the trade-off stays measurable.
Happy to be shown a workload where this actually matters.
Measurements gathered with Claude Code; methods/scripts available on request.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
@henryiii and @hugovk , I don't feel like the justification case is strong, so I'd like to revert this to its prior, direct, more concise, single-expression, debt-free implementation using higher-level constructs. I'll let this sit for a while to give a chance to make the case (DO NOT MERGE before 2026-08-31), but my instinct is that we should only hyper optimize where the case is clear. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
Consider reverting optimization in normalization
There was an error while loading. Please reload this page.