-
-
Notifications
You must be signed in to change notification settings - Fork 33.9k
gh-143658: importlib.metadata: Use str.translate to improve performance of importlib.metadata.Prepared.normalized
#143660
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
gh-143658: importlib.metadata: Use str.translate to improve performance of importlib.metadata.Prepared.normalized
#143660
Conversation
Co-Authored-By: Henry Schreiner <henryschreineriii@gmail.com>
translate to improve performance of canonicalize_nametranslate to improve performance of canonicalize_name
Misc/NEWS.d/next/Library/2026-01-10-15-40-57.gh-issue-143658.Ox6pE5.rst
Outdated
Show resolved
Hide resolved
translate to improve performance of canonicalize_namestr.translate to improve performance of importlib.metadata.Prepared.normalized
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
picnixz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have tests actually? if not, maybe it'd be good to add some.
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
johnslavik
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small ideas
Co-authored-by: Bartosz Sławecki <bartosz@ilikepython.com>
| PEP 503 normalization plus dashes as underscores. | ||
| """ | ||
| return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_') | ||
| # Emulates ``re.sub(r"[-_.]+", "-", name).lower()`` from PEP 503 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hugovk I did a quick scan of the 8.34M package names, and 3.17M are purely lowercase with no separators. Given that, I tried to add a fast path check here before we normalize the table and found strong improvements in the benchmark. I think the most readable version of the fast path would be:
if name.islower() and name.isalnum():
return name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's worth it. What Hugo suggested is readable enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not an unreasonable position. My reasoning was that a significant portion of packages (roughly 38%) are already alphanumeric and lowercase. This fast path allows skipping the translation and loop overhead for the most common case. I felt the performance gain for those users justified the small increase in complexity, but I'm happy to defer to your preference on the balance between speed and code footprint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How much performance gain are we speaking about though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're very close. If anything, the "fast path" seems to be a bit slower :)
❯ # main
❯ ./python.exe -m timeit -s "from importlib.metadata import Prepared" "Prepared.normalize('pillow')"
1000000 loops, best of 5: 390 nsec per loop
❯ ./python.exe -m timeit -s "from importlib.metadata import Prepared" "Prepared.normalize('pillow')"
1000000 loops, best of 5: 393 nsec per loop❯ # PR
❯ ./python.exe -m timeit -s "from importlib.metadata import Prepared" "Prepared.normalize('pillow')"
5000000 loops, best of 5: 95.8 nsec per loop
❯ ./python.exe -m timeit -s "from importlib.metadata import Prepared" "Prepared.normalize('pillow')"
5000000 loops, best of 5: 96 nsec per loop❯ # fast path
❯ ./python.exe -m timeit -s "from importlib.metadata import Prepared" "Prepared.normalize('pillow')"
5000000 loops, best of 5: 94.3 nsec per loop
❯ ./python.exe -m timeit -s "from importlib.metadata import Prepared" "Prepared.normalize('pillow')"
5000000 loops, best of 5: 97.5 nsec per loop❯ hyperfine --warmup 1 --runs 3 \
--prepare "git checkout main" "./python.exe benchmark_names_stdlib.py # main" \
--prepare "git checkout 3.15-importlib.metadata-canonicalize_name" "./python.exe benchmark_names_stdlib.py # PR" \
--prepare "git checkout 3.15-importlib.metadata-canonicalize_name-fast-path" "./python.exe benchmark_names_stdlib.py # fast path"
Benchmark 1: ./python.exe benchmark_names_stdlib.py # main
Time (mean ± σ): 5.633 s ± 0.046 s [User: 5.491 s, System: 0.101 s]
Range (min … max): 5.592 s … 5.683 s 3 runs
Benchmark 2: ./python.exe benchmark_names_stdlib.py # PR
Time (mean ± σ): 1.879 s ± 0.026 s [User: 1.783 s, System: 0.081 s]
Range (min … max): 1.858 s … 1.907 s 3 runs
Benchmark 3: ./python.exe benchmark_names_stdlib.py # fast path
Time (mean ± σ): 1.952 s ± 0.005 s [User: 1.863 s, System: 0.080 s]
Range (min … max): 1.947 s … 1.957 s 3 runs
Summary
./python.exe benchmark_names_stdlib.py # PR ran
1.04 ± 0.01 times faster than ./python.exe benchmark_names_stdlib.py # fast path
3.00 ± 0.05 times faster than ./python.exe benchmark_names_stdlib.py # main
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Running a slightly modified benchmark as the above (timeit + best of 3), on my Macbook (Apple Silicon), main branch with debug build of cPython:
- Current PR (Translate + Loop) 5.4756s
- With Fast Path (isalnum) 4.4691s
So for me, about 18.4% reduction in total time (or +22% speedup) on the full pypi benchmarl.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, we posted at about the same time. Interesting results from my end compared to yours, but would considers your canonical (especially since I'm on debug build with the extra overhead), so please disregard my comments then @hugovk :)
We can apply @henryiii's improvement to
packagingin pypa/packaging#1030 (see also https://iscinumpy.dev/post/packaging-faster/) to improve the performance ofcanonicalize_nameand make it ~3.7 times faster.Benchmark
Run
Prepared.normalize(n)on every name in PyPI:Benchmark data can be found at https://gist.github.com/hugovk/efdbee0620cc64df7b405b52cf0b6e42
Before
With optimisations:
After
3.7 times faster.
str.translateto improve performance ofimportlib.metadata.Prepared.normalized#143658