Skip to content

perf: simply Specifier regex#1106

Merged
henryiii merged 1 commit intopypa:mainfrom
sirosen:micro-optimize-specifiers
Mar 9, 2026
Merged

perf: simply Specifier regex#1106
henryiii merged 1 commit intopypa:mainfrom
sirosen:micro-optimize-specifiers

Conversation

@sirosen
Copy link
Copy Markdown
Contributor

@sirosen sirosen commented Mar 7, 2026

I'm not totally sure I trust the results I'm getting from benchmarking.
These results seem too good for the modest changes included here, so I'm concerned that I've somehow run the benchmarks incorrectly.

However, it's showing a consistent improvement, so I think this is worth submitting for further evaluation.


Examining the specifier regex, two potential optimizations seem
worthwhile:

  1. Negative lookbehinds can be eliminated if we combine the operator
    regex with the version regex -- this avoids crawling the same
    characters of a string forwards and backwards.

  2. Do more string operations, like strip() rather than having the
    regex engine do this work.

These ideas are somewhat combined, in that getting rid of the lookbehinds
only really works by eliminating the group selectors in use. And removing
the group selectors in the regex requires that we do more string
operations.

This also interestingly results in a regex stored in the Specifier
which matches its secondary usage in _tokenizer, which was previously
very slightly misaligned (in that the group captures in the regex were
unused).


Running the benchmarks from #1059 on this, I get some surprisingly good results.

We expect this to be mostly noticeable with the Specifier constructor benchmark, and that does show results:

| Change   | Before [b2986d4b] <main>   | After [8feee4e6] <micro-optimize-specifiers>   | Ratio   | Benchmark (Parameter)                                                     |
|----------|----------------------------|------------------------------------------------|---------|---------------------------------------------------------------------------|
| -        | 4.87±0.6ms                 | 4.17±0.04ms                                    | 0.86    | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.10]         |
| -        | 4.16±0.04ms                | 3.24±0.04ms                                    | 0.78    | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.11]         |
| -        | 4.21±0.1ms                 | 3.37±0.04ms                                    | 0.80    | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.12]         |
| -        | 4.16±0.08ms                | 3.29±0.07ms                                    | 0.79    | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.13]         |
| -        | 3.47±0.01ms                | 2.86±0.05ms                                    | 0.82    | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.14]         |
|          | 4.69±0.2ms                 | 4.57±0.1ms                                     | 0.97    | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.8]          |
|          | 4.87±0.07ms                | 4.65±0.04ms                                    | 0.96    | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.9]          |

So this makes this branch look much better, especially on newer Pythons, and no regression (but no real improvement) in 3.8 and 3.9.

Many of the higher level tests show similar/related improvement. I think the largest ratio in my runs is this one:

| Change   | Before [b2986d4b] <main>   | After [8feee4e6] <micro-optimize-specifiers>   | Ratio   | Benchmark (Parameter)                                                     |
|----------|----------------------------|------------------------------------------------|---------|---------------------------------------------------------------------------|
| -        | 800±9μs                    | 611±9μs                                        | 0.76    | resolver.TimeResolverSuite.time_resolver_loop [zug/virtualenv-py3.11]     |
full asv results
$ asv compare 'main' 'micro-optimize-specifiers'
<frozen importlib._bootstrap>:491: RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'asv._rangemedian', which has not declared that it can run safely without the GIL. To override this behavior and keep the GIL disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.

All benchmarks:

| Change   | Before [b2986d4b] <main>   | After [8feee4e6] <micro-optimize-specifiers>   | Ratio   | Benchmark (Parameter)                                                     |
|----------|----------------------------|------------------------------------------------|---------|---------------------------------------------------------------------------|
| -        | 3.92±0.1ms                 | 3.47±0.03ms                                    | 0.88    | markers.TimeMarkerSuite.time_constructor [zug/virtualenv-py3.10]          |
| -        | 3.08±0.03ms                | 2.65±0.02ms                                    | 0.86    | markers.TimeMarkerSuite.time_constructor [zug/virtualenv-py3.11]          |
| -        | 3.47±0.1ms                 | 2.88±0.07ms                                    | 0.83    | markers.TimeMarkerSuite.time_constructor [zug/virtualenv-py3.12]          |
| -        | 3.41±0.05ms                | 2.97±0.05ms                                    | 0.87    | markers.TimeMarkerSuite.time_constructor [zug/virtualenv-py3.13]          |
| -        | 2.94±0.05ms                | 2.55±0.05ms                                    | 0.87    | markers.TimeMarkerSuite.time_constructor [zug/virtualenv-py3.14]          |
|          | 3.83±0.04ms                | 3.63±0.1ms                                     | 0.95    | markers.TimeMarkerSuite.time_constructor [zug/virtualenv-py3.8]           |
|          | 4.00±0.03ms                | 3.78±0.03ms                                    | 0.94    | markers.TimeMarkerSuite.time_constructor [zug/virtualenv-py3.9]           |
|          | 1.36±0.1ms                 | 1.16±0.02ms                                    | ~0.86   | markers.TimeMarkerSuite.time_evaluate [zug/virtualenv-py3.10]             |
| -        | 1.01±0ms                   | 833±30μs                                       | 0.83    | markers.TimeMarkerSuite.time_evaluate [zug/virtualenv-py3.11]             |
| -        | 1.15±0.05ms                | 965±30μs                                       | 0.84    | markers.TimeMarkerSuite.time_evaluate [zug/virtualenv-py3.12]             |
| -        | 1.10±0.03ms                | 919±20μs                                       | 0.83    | markers.TimeMarkerSuite.time_evaluate [zug/virtualenv-py3.13]             |
| -        | 894±20μs                   | 747±3μs                                        | 0.84    | markers.TimeMarkerSuite.time_evaluate [zug/virtualenv-py3.14]             |
|          | 1.23±0.01ms                | 1.15±0.03ms                                    | 0.93    | markers.TimeMarkerSuite.time_evaluate [zug/virtualenv-py3.8]              |
|          | 1.24±0.01ms                | 1.19±0.01ms                                    | 0.96    | markers.TimeMarkerSuite.time_evaluate [zug/virtualenv-py3.9]              |
|          | 17.2±2ms                   | 14.8±0.09ms                                    | ~0.86   | requirement.TimeRequirementSuite.time_constructor [zug/virtualenv-py3.10] |
| -        | 14.7±0.2ms                 | 11.5±0.2ms                                     | 0.78    | requirement.TimeRequirementSuite.time_constructor [zug/virtualenv-py3.11] |
| -        | 16.2±0.07ms                | 12.5±0.07ms                                    | 0.77    | requirement.TimeRequirementSuite.time_constructor [zug/virtualenv-py3.12] |
| -        | 15.4±0.1ms                 | 12.6±0.1ms                                     | 0.82    | requirement.TimeRequirementSuite.time_constructor [zug/virtualenv-py3.13] |
| -        | 12.7±0.2ms                 | 10.4±0.03ms                                    | 0.82    | requirement.TimeRequirementSuite.time_constructor [zug/virtualenv-py3.14] |
|          | 17.3±0.1ms                 | 16.0±0.2ms                                     | 0.92    | requirement.TimeRequirementSuite.time_constructor [zug/virtualenv-py3.8]  |
|          | 17.4±0.1ms                 | 16.2±0.2ms                                     | 0.93    | requirement.TimeRequirementSuite.time_constructor [zug/virtualenv-py3.9]  |
| -        | 991±90μs                   | 817±4μs                                        | 0.82    | resolver.TimeResolverSuite.time_resolver_loop [zug/virtualenv-py3.10]     |
| -        | 800±9μs                    | 611±9μs                                        | 0.76    | resolver.TimeResolverSuite.time_resolver_loop [zug/virtualenv-py3.11]     |
| -        | 791±3μs                    | 622±8μs                                        | 0.79    | resolver.TimeResolverSuite.time_resolver_loop [zug/virtualenv-py3.12]     |
| -        | 775±20μs                   | 646±10μs                                       | 0.83    | resolver.TimeResolverSuite.time_resolver_loop [zug/virtualenv-py3.13]     |
| -        | 624±8μs                    | 515±2μs                                        | 0.82    | resolver.TimeResolverSuite.time_resolver_loop [zug/virtualenv-py3.14]     |
|          | 906±10μs                   | 844±9μs                                        | 0.93    | resolver.TimeResolverSuite.time_resolver_loop [zug/virtualenv-py3.8]      |
|          | 921±20μs                   | 908±10μs                                       | 0.99    | resolver.TimeResolverSuite.time_resolver_loop [zug/virtualenv-py3.9]      |
| -        | 4.87±0.6ms                 | 4.17±0.04ms                                    | 0.86    | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.10]         |
| -        | 4.16±0.04ms                | 3.24±0.04ms                                    | 0.78    | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.11]         |
| -        | 4.21±0.1ms                 | 3.37±0.04ms                                    | 0.80    | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.12]         |
| -        | 4.16±0.08ms                | 3.29±0.07ms                                    | 0.79    | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.13]         |
| -        | 3.47±0.01ms                | 2.86±0.05ms                                    | 0.82    | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.14]         |
|          | 4.69±0.2ms                 | 4.57±0.1ms                                     | 0.97    | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.8]          |
|          | 4.87±0.07ms                | 4.65±0.04ms                                    | 0.96    | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.9]          |
| -        | 6.17±0.6ms                 | 5.32±0.02ms                                    | 0.86    | specifiers.TimeSpecSuite.time_contains_cold [zug/virtualenv-py3.10]       |
| -        | 5.09±0.03ms                | 4.21±0.04ms                                    | 0.83    | specifiers.TimeSpecSuite.time_contains_cold [zug/virtualenv-py3.11]       |
| -        | 5.71±0.2ms                 | 4.44±0.1ms                                     | 0.78    | specifiers.TimeSpecSuite.time_contains_cold [zug/virtualenv-py3.12]       |
| -        | 5.42±0.05ms                | 4.64±0.08ms                                    | 0.86    | specifiers.TimeSpecSuite.time_contains_cold [zug/virtualenv-py3.13]       |
| -        | 4.24±0.09ms                | 3.46±0.03ms                                    | 0.82    | specifiers.TimeSpecSuite.time_contains_cold [zug/virtualenv-py3.14]       |
|          | 6.44±0.08ms                | 6.04±0.04ms                                    | 0.94    | specifiers.TimeSpecSuite.time_contains_cold [zug/virtualenv-py3.8]        |
|          | 6.38±0.06ms                | 6.04±0.1ms                                     | 0.95    | specifiers.TimeSpecSuite.time_contains_cold [zug/virtualenv-py3.9]        |
|          | 4.71±0.6ms                 | 4.10±0.04ms                                    | ~0.87   | specifiers.TimeSpecSuite.time_contains_warm [zug/virtualenv-py3.10]       |
| -        | 3.86±0.02ms                | 3.16±0.03ms                                    | 0.82    | specifiers.TimeSpecSuite.time_contains_warm [zug/virtualenv-py3.11]       |
| -        | 4.29±0.07ms                | 3.43±0.05ms                                    | 0.80    | specifiers.TimeSpecSuite.time_contains_warm [zug/virtualenv-py3.12]       |
| -        | 4.01±0.04ms                | 3.44±0.02ms                                    | 0.86    | specifiers.TimeSpecSuite.time_contains_warm [zug/virtualenv-py3.13]       |
| -        | 3.14±0.02ms                | 2.56±0.01ms                                    | 0.82    | specifiers.TimeSpecSuite.time_contains_warm [zug/virtualenv-py3.14]       |
|          | 4.77±0.07ms                | 4.37±0.07ms                                    | 0.92    | specifiers.TimeSpecSuite.time_contains_warm [zug/virtualenv-py3.8]        |
|          | 4.54±0.09ms                | 4.48±0.02ms                                    | 0.99    | specifiers.TimeSpecSuite.time_contains_warm [zug/virtualenv-py3.9]        |
|          | 14.2±2μs                   | 12.2±0.09μs                                    | ~0.86   | specifiers.TimeSpecSuite.time_filter_simple_cold [zug/virtualenv-py3.10]  |
| -        | 10.7±0.2μs                 | 8.09±0.03μs                                    | 0.75    | specifiers.TimeSpecSuite.time_filter_simple_cold [zug/virtualenv-py3.11]  |
| -        | 9.78±0.2μs                 | 7.93±0.1μs                                     | 0.81    | specifiers.TimeSpecSuite.time_filter_simple_cold [zug/virtualenv-py3.12]  |
| -        | 9.99±0.1μs                 | 8.44±0.1μs                                     | 0.85    | specifiers.TimeSpecSuite.time_filter_simple_cold [zug/virtualenv-py3.13]  |
| -        | 7.89±0.02μs                | 6.33±0.2μs                                     | 0.80    | specifiers.TimeSpecSuite.time_filter_simple_cold [zug/virtualenv-py3.14]  |
|          | 13.7±0.2μs                 | 13.0±0.2μs                                     | 0.95    | specifiers.TimeSpecSuite.time_filter_simple_cold [zug/virtualenv-py3.8]   |
|          | 13.6±0.09μs                | 13.2±0.04μs                                    | 0.97    | specifiers.TimeSpecSuite.time_filter_simple_cold [zug/virtualenv-py3.9]   |
|          | 12.3±1μs                   | 10.6±0.09μs                                    | ~0.86   | specifiers.TimeSpecSuite.time_filter_simple_warm [zug/virtualenv-py3.10]  |
| -        | 9.12±0.06μs                | 7.06±0.04μs                                    | 0.77    | specifiers.TimeSpecSuite.time_filter_simple_warm [zug/virtualenv-py3.11]  |
| -        | 8.22±0.07μs                | 6.80±0.1μs                                     | 0.83    | specifiers.TimeSpecSuite.time_filter_simple_warm [zug/virtualenv-py3.12]  |
| -        | 8.67±0.1μs                 | 7.17±0.1μs                                     | 0.83    | specifiers.TimeSpecSuite.time_filter_simple_warm [zug/virtualenv-py3.13]  |
| -        | 6.68±0.04μs                | 5.54±0.07μs                                    | 0.83    | specifiers.TimeSpecSuite.time_filter_simple_warm [zug/virtualenv-py3.14]  |
|          | 11.7±0.3μs                 | 11.2±0.2μs                                     | 0.96    | specifiers.TimeSpecSuite.time_filter_simple_warm [zug/virtualenv-py3.8]   |
|          | 11.7±0.1μs                 | 11.6±0.4μs                                     | 0.99    | specifiers.TimeSpecSuite.time_filter_simple_warm [zug/virtualenv-py3.9]   |
| -        | 3.81±0.3μs                 | 3.22±0.02μs                                    | 0.85    | utils.TimeUtils.time_canonicalize_name [zug/virtualenv-py3.10]            |
| -        | 3.23±0.01μs                | 2.56±0.01μs                                    | 0.79    | utils.TimeUtils.time_canonicalize_name [zug/virtualenv-py3.11]            |
| -        | 3.39±0.02μs                | 2.74±0.02μs                                    | 0.81    | utils.TimeUtils.time_canonicalize_name [zug/virtualenv-py3.12]            |
| -        | 3.56±0.06μs                | 3.04±0.06μs                                    | 0.85    | utils.TimeUtils.time_canonicalize_name [zug/virtualenv-py3.13]            |
|          | 3.06±0.05μs                | 2.55±0.06μs                                    | ~0.83   | utils.TimeUtils.time_canonicalize_name [zug/virtualenv-py3.14]            |
|          | 3.99±0.05μs                | 3.80±0.02μs                                    | 0.95    | utils.TimeUtils.time_canonicalize_name [zug/virtualenv-py3.8]             |
|          | 3.97±0.02μs                | 3.79±0.03μs                                    | 0.95    | utils.TimeUtils.time_canonicalize_name [zug/virtualenv-py3.9]             |
|          | 1.96±0.3ms                 | 1.73±0.02ms                                    | ~0.88   | version.TimeVersionSuite.time_constructor [zug/virtualenv-py3.10]         |
| -        | 1.40±0.02ms                | 1.15±0.01ms                                    | 0.82    | version.TimeVersionSuite.time_constructor [zug/virtualenv-py3.11]         |
| -        | 1.80±0.03ms                | 1.38±0.06ms                                    | 0.76    | version.TimeVersionSuite.time_constructor [zug/virtualenv-py3.12]         |
| -        | 1.56±0.01ms                | 1.30±0.03ms                                    | 0.83    | version.TimeVersionSuite.time_constructor [zug/virtualenv-py3.13]         |
| -        | 1.33±0ms                   | 1.10±0.03ms                                    | 0.83    | version.TimeVersionSuite.time_constructor [zug/virtualenv-py3.14]         |
|          | 2.07±0.07ms                | 1.96±0.01ms                                    | 0.95    | version.TimeVersionSuite.time_constructor [zug/virtualenv-py3.8]          |
|          | 2.04±0.01ms                | 1.95±0.04ms                                    | 0.96    | version.TimeVersionSuite.time_constructor [zug/virtualenv-py3.9]          |
|          | 3.37±0.3ms                 | 2.95±0.03ms                                    | ~0.88   | version.TimeVersionSuite.time_hash [zug/virtualenv-py3.10]                |
| -        | 2.51±0.05ms                | 2.02±0.02ms                                    | 0.80    | version.TimeVersionSuite.time_hash [zug/virtualenv-py3.11]                |
| -        | 3.06±0.01ms                | 2.45±0.03ms                                    | 0.80    | version.TimeVersionSuite.time_hash [zug/virtualenv-py3.12]                |
| -        | 2.92±0.06ms                | 2.42±0.01ms                                    | 0.83    | version.TimeVersionSuite.time_hash [zug/virtualenv-py3.13]                |
| -        | 2.33±0.01ms                | 1.95±0.01ms                                    | 0.83    | version.TimeVersionSuite.time_hash [zug/virtualenv-py3.14]                |
|          | 3.43±0.05ms                | 3.33±0.09ms                                    | 0.97    | version.TimeVersionSuite.time_hash [zug/virtualenv-py3.8]                 |
|          | 3.49±0.05ms                | 3.29±0.09ms                                    | 0.94    | version.TimeVersionSuite.time_hash [zug/virtualenv-py3.9]                 |
|          | 2.21±0.3ms                 | 1.88±0.01ms                                    | ~0.85   | version.TimeVersionSuite.time_sort_cold [zug/virtualenv-py3.10]           |
| -        | 1.64±0.03ms                | 1.28±0.01ms                                    | 0.78    | version.TimeVersionSuite.time_sort_cold [zug/virtualenv-py3.11]           |
| -        | 1.99±0ms                   | 1.61±0.02ms                                    | 0.81    | version.TimeVersionSuite.time_sort_cold [zug/virtualenv-py3.12]           |
| -        | 2.05±0.01ms                | 1.71±0.02ms                                    | 0.84    | version.TimeVersionSuite.time_sort_cold [zug/virtualenv-py3.13]           |
| -        | 1.70±0ms                   | 1.38±0.01ms                                    | 0.81    | version.TimeVersionSuite.time_sort_cold [zug/virtualenv-py3.14]           |
|          | 2.57±0.05ms                | 2.43±0.04ms                                    | 0.94    | version.TimeVersionSuite.time_sort_cold [zug/virtualenv-py3.8]            |
|          | 2.54±0.02ms                | 2.50±0.03ms                                    | 0.99    | version.TimeVersionSuite.time_sort_cold [zug/virtualenv-py3.9]            |
| -        | 1.59±0.2ms                 | 1.35±0.02ms                                    | 0.85    | version.TimeVersionSuite.time_sort_warm [zug/virtualenv-py3.10]           |
| -        | 1.34±0.02ms                | 1.05±0.01ms                                    | 0.79    | version.TimeVersionSuite.time_sort_warm [zug/virtualenv-py3.11]           |
| -        | 1.63±0ms                   | 1.33±0.02ms                                    | 0.81    | version.TimeVersionSuite.time_sort_warm [zug/virtualenv-py3.12]           |
| -        | 1.69±0.03ms                | 1.38±0.02ms                                    | 0.82    | version.TimeVersionSuite.time_sort_warm [zug/virtualenv-py3.13]           |
| -        | 1.37±0ms                   | 1.13±0.01ms                                    | 0.82    | version.TimeVersionSuite.time_sort_warm [zug/virtualenv-py3.14]           |
|          | 2.03±0.01ms                | 1.91±0.04ms                                    | 0.94    | version.TimeVersionSuite.time_sort_warm [zug/virtualenv-py3.8]            |
|          | 2.00±0.02ms                | 1.93±0.01ms                                    | 0.96    | version.TimeVersionSuite.time_sort_warm [zug/virtualenv-py3.9]            |
| -        | 3.31±0.3ms                 | 2.76±0.02ms                                    | 0.83    | version.TimeVersionSuite.time_str [zug/virtualenv-py3.10]                 |
| -        | 2.47±0.04ms                | 1.96±0.01ms                                    | 0.79    | version.TimeVersionSuite.time_str [zug/virtualenv-py3.11]                 |
| -        | 2.83±0.04ms                | 2.27±0.02ms                                    | 0.80    | version.TimeVersionSuite.time_str [zug/virtualenv-py3.12]                 |
| -        | 2.60±0.02ms                | 2.09±0.01ms                                    | 0.80    | version.TimeVersionSuite.time_str [zug/virtualenv-py3.13]                 |
| -        | 2.15±0.02ms                | 1.79±0.02ms                                    | 0.83    | version.TimeVersionSuite.time_str [zug/virtualenv-py3.14]                 |
|          | 3.45±0.03ms                | 3.40±0.08ms                                    | 0.99    | version.TimeVersionSuite.time_str [zug/virtualenv-py3.8]                  |
|          | 3.50±0.02ms                | 3.32±0.03ms                                    | 0.95    | version.TimeVersionSuite.time_str [zug/virtualenv-py3.9]                  |

@henryiii henryiii force-pushed the micro-optimize-specifiers branch from 8feee4e to 77ab747 Compare March 7, 2026 07:04
@henryiii
Copy link
Copy Markdown
Contributor

henryiii commented Mar 7, 2026

https://github.com/pypa/packaging/actions/runs/22794380340?pr=1106

I think you’re comparing an old run branch somehow.

And improving specifiers won’t make everything, like versions, faster. :)

@sirosen
Copy link
Copy Markdown
Contributor Author

sirosen commented Mar 7, 2026

Okay, that's much more in line with what I expected, maybe even a bit worse. Marginal, maybe no improvement.
I don't quite understand why I consistently get so much better results from local runs.

I think I should close this, but I won't rush that in case you think it might be worth salvaging.

@henryiii
Copy link
Copy Markdown
Contributor

henryiii commented Mar 7, 2026

Have you tried deleting html and results and rerunning?

@henryiii
Copy link
Copy Markdown
Contributor

henryiii commented Mar 7, 2026

(And yes, I think a readability focus for this PR might be salvageable)

@sirosen
Copy link
Copy Markdown
Contributor Author

sirosen commented Mar 7, 2026

I just cleared results and did a fresh run, and my results are 10% worse than main now. I don't know what the source of the high variance is -- probably I should have been more thorough in spinning everything down for benchmarking. I'll try to figure it out, but for now I think we can simply discount my local runs as not being trustworthy.


Looking at this for readability, I like the regex changes -- I think those lookbehinds are harder to read than this version -- but I don't love the string slicing. I'll try to think if there's a way to make that tidier.

@sirosen sirosen marked this pull request as draft March 7, 2026 16:42
@sirosen sirosen force-pushed the micro-optimize-specifiers branch from 77ab747 to 7e9aafb Compare March 7, 2026 23:25
@sirosen
Copy link
Copy Markdown
Contributor Author

sirosen commented Mar 8, 2026

I think this may actually be a small perf gain, but not big enough to show up in the CI benchmarking.

I rebased to grab the latest changes, shut down most non-essential applications, and ran

$ asv run 'main^1..HEAD' --interleave-rounds --attribute 'rounds=10'

There are no results with a >10% change (the ASV default), but with a more sensitive threshold of 5%, we see some info:

$ asv compare 'main' 'micro-optimize-specifiers' --only-changed --factor 1.05
| Change   | Before [f59d6ee9] <main>   | After [7e9aafba] <micro-optimize-specifiers>   |   Ratio | Benchmark (Parameter)                                                             |
|----------|----------------------------|------------------------------------------------|---------|-----------------------------------------------------------------------------------|
| -        | 729±80μs                   | 694±10μs                                       |    0.95 | markers.TimeMarkerSuite.time_evaluate [zug/virtualenv-py3.14-PYTHONHASHSEED0]     |
| -        | 1.52±0.2ms                 | 1.39±0.04ms                                    |    0.91 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.13-PYTHONHASHSEED0] |
| -        | 1.27±0.2ms                 | 1.18±0.03ms                                    |    0.93 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.14-PYTHONHASHSEED0] |

I'm starting to actually believe these numbers.

The results for the Specifier constructor benchmark, across all Pythons, show a small improvement on the latest Pythons and an even smaller slowdown on 3.8 and 3.9. That's consistent with some of the behavior I've seen with string operations which have fast-path implementations in newer Pythons. (I think this is part of Faster CPython, but it might just be the ordinary improvement process for CPython.)

results for just the Specifier constructor
| Change   | Before [f59d6ee9] <main>   | After [7e9aafba] <micro-optimize-specifiers>   |   Ratio | Benchmark (Parameter)                                                                     |
|----------|----------------------------|------------------------------------------------|---------|-------------------------------------------------------------------------------------------|
|          | 1.48±0.2ms                 | 1.46±0.02ms                                    |    0.99 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.10-PYTHONHASHSEED0]         |
|          | 1.37±0.2ms                 | 1.34±0.03ms                                    |    0.98 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.11-PYTHONHASHSEED0]         |
|          | 1.52±0.2ms                 | 1.54±0.06ms                                    |    1.01 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.12-PYTHONHASHSEED0]         |
|          | 1.52±0.2ms                 | 1.39±0.04ms                                    |    0.91 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.13-PYTHONHASHSEED0]         |
|          | 1.27±0.2ms                 | 1.18±0.03ms                                    |    0.93 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.14-PYTHONHASHSEED0]         |
|          | 1.73±0.04ms                | 1.76±0.2ms                                     |    1.02 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.8-PYTHONHASHSEED0]          |
|          | 1.66±0.2ms                 | 1.68±0.2ms                                     |    1.01 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.9-PYTHONHASHSEED0]          |
all benchmarks from this run
| Change   | Before [f59d6ee9] <main>   | After [7e9aafba] <micro-optimize-specifiers>   |   Ratio | Benchmark (Parameter)                                                                     |
|----------|----------------------------|------------------------------------------------|---------|-------------------------------------------------------------------------------------------|
|          | 3.34±0.4ms                 | 3.34±0.1ms                                     |    1    | markers.TimeMarkerSuite.time_constructor [zug/virtualenv-py3.10-PYTHONHASHSEED0]          |
|          | 2.40±0.2ms                 | 2.36±0.06ms                                    |    0.98 | markers.TimeMarkerSuite.time_constructor [zug/virtualenv-py3.11-PYTHONHASHSEED0]          |
|          | 2.72±0.3ms                 | 2.70±0.08ms                                    |    0.99 | markers.TimeMarkerSuite.time_constructor [zug/virtualenv-py3.12-PYTHONHASHSEED0]          |
|          | 2.70±0.2ms                 | 2.64±0.08ms                                    |    0.98 | markers.TimeMarkerSuite.time_constructor [zug/virtualenv-py3.13-PYTHONHASHSEED0]          |
|          | 2.38±0.2ms                 | 2.31±0.05ms                                    |    0.97 | markers.TimeMarkerSuite.time_constructor [zug/virtualenv-py3.14-PYTHONHASHSEED0]          |
|          | 3.95±0.08ms                | 3.94±0.3ms                                     |    1    | markers.TimeMarkerSuite.time_constructor [zug/virtualenv-py3.8-PYTHONHASHSEED0]           |
|          | 3.86±0.4ms                 | 3.79±0.3ms                                     |    0.98 | markers.TimeMarkerSuite.time_constructor [zug/virtualenv-py3.9-PYTHONHASHSEED0]           |
|          | 1.12±0.09ms                | 1.09±0.03ms                                    |    0.98 | markers.TimeMarkerSuite.time_evaluate [zug/virtualenv-py3.10-PYTHONHASHSEED0]             |
|          | 821±80μs                   | 793±20μs                                       |    0.97 | markers.TimeMarkerSuite.time_evaluate [zug/virtualenv-py3.11-PYTHONHASHSEED0]             |
|          | 881±90μs                   | 865±30μs                                       |    0.98 | markers.TimeMarkerSuite.time_evaluate [zug/virtualenv-py3.12-PYTHONHASHSEED0]             |
|          | 844±90μs                   | 826±40μs                                       |    0.98 | markers.TimeMarkerSuite.time_evaluate [zug/virtualenv-py3.13-PYTHONHASHSEED0]             |
|          | 729±80μs                   | 694±10μs                                       |    0.95 | markers.TimeMarkerSuite.time_evaluate [zug/virtualenv-py3.14-PYTHONHASHSEED0]             |
|          | 1.23±0.03ms                | 1.23±0.1ms                                     |    1    | markers.TimeMarkerSuite.time_evaluate [zug/virtualenv-py3.8-PYTHONHASHSEED0]              |
|          | 1.21±0.1ms                 | 1.20±0.1ms                                     |    0.99 | markers.TimeMarkerSuite.time_evaluate [zug/virtualenv-py3.9-PYTHONHASHSEED0]              |
|          | 11.8±1ms                   | 11.6±0.2ms                                     |    0.98 | requirement.TimeRequirementSuite.time_constructor [zug/virtualenv-py3.10-PYTHONHASHSEED0] |
|          | 8.58±0.8ms                 | 8.43±0.2ms                                     |    0.98 | requirement.TimeRequirementSuite.time_constructor [zug/virtualenv-py3.11-PYTHONHASHSEED0] |
|          | 9.44±1ms                   | 9.24±0.2ms                                     |    0.98 | requirement.TimeRequirementSuite.time_constructor [zug/virtualenv-py3.12-PYTHONHASHSEED0] |
|          | 9.55±1ms                   | 9.13±0.2ms                                     |    0.96 | requirement.TimeRequirementSuite.time_constructor [zug/virtualenv-py3.13-PYTHONHASHSEED0] |
|          | 7.71±0.9ms                 | 7.42±0.1ms                                     |    0.96 | requirement.TimeRequirementSuite.time_constructor [zug/virtualenv-py3.14-PYTHONHASHSEED0] |
|          | 13.6±0.4ms                 | 13.5±1ms                                       |    0.99 | requirement.TimeRequirementSuite.time_constructor [zug/virtualenv-py3.8-PYTHONHASHSEED0]  |
|          | 13.4±2ms                   | 13.3±1ms                                       |    0.99 | requirement.TimeRequirementSuite.time_constructor [zug/virtualenv-py3.9-PYTHONHASHSEED0]  |
|          | 727±80μs                   | 717±20μs                                       |    0.99 | resolver.TimeResolverSuite.time_resolver_loop [zug/virtualenv-py3.10-PYTHONHASHSEED0]     |
|          | 546±50μs                   | 543±20μs                                       |    1    | resolver.TimeResolverSuite.time_resolver_loop [zug/virtualenv-py3.11-PYTHONHASHSEED0]     |
|          | 546±60μs                   | 543±20μs                                       |    0.99 | resolver.TimeResolverSuite.time_resolver_loop [zug/virtualenv-py3.12-PYTHONHASHSEED0]     |
|          | 566±70μs                   | 541±20μs                                       |    0.96 | resolver.TimeResolverSuite.time_resolver_loop [zug/virtualenv-py3.13-PYTHONHASHSEED0]     |
|          | 434±40μs                   | 428±9μs                                        |    0.99 | resolver.TimeResolverSuite.time_resolver_loop [zug/virtualenv-py3.14-PYTHONHASHSEED0]     |
|          | 858±20μs                   | 851±90μs                                       |    0.99 | resolver.TimeResolverSuite.time_resolver_loop [zug/virtualenv-py3.8-PYTHONHASHSEED0]      |
|          | 834±100μs                  | 832±100μs                                      |    1    | resolver.TimeResolverSuite.time_resolver_loop [zug/virtualenv-py3.9-PYTHONHASHSEED0]      |
|          | 1.48±0.2ms                 | 1.46±0.02ms                                    |    0.99 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.10-PYTHONHASHSEED0]         |
|          | 1.37±0.2ms                 | 1.34±0.03ms                                    |    0.98 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.11-PYTHONHASHSEED0]         |
|          | 1.52±0.2ms                 | 1.54±0.06ms                                    |    1.01 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.12-PYTHONHASHSEED0]         |
|          | 1.52±0.2ms                 | 1.39±0.04ms                                    |    0.91 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.13-PYTHONHASHSEED0]         |
|          | 1.27±0.2ms                 | 1.18±0.03ms                                    |    0.93 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.14-PYTHONHASHSEED0]         |
|          | 1.73±0.04ms                | 1.76±0.2ms                                     |    1.02 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.8-PYTHONHASHSEED0]          |
|          | 1.66±0.2ms                 | 1.68±0.2ms                                     |    1.01 | specifiers.TimeSpecSuite.time_constructor [zug/virtualenv-py3.9-PYTHONHASHSEED0]          |
|          | 5.31±0.7ms                 | 5.31±0.2ms                                     |    1    | specifiers.TimeSpecSuite.time_contains_cold [zug/virtualenv-py3.10-PYTHONHASHSEED0]       |
|          | 3.90±0.4ms                 | 3.90±0.1ms                                     |    1    | specifiers.TimeSpecSuite.time_contains_cold [zug/virtualenv-py3.11-PYTHONHASHSEED0]       |
|          | 4.15±0.4ms                 | 4.15±0.08ms                                    |    1    | specifiers.TimeSpecSuite.time_contains_cold [zug/virtualenv-py3.12-PYTHONHASHSEED0]       |
|          | 4.14±0.5ms                 | 4.00±0.1ms                                     |    0.97 | specifiers.TimeSpecSuite.time_contains_cold [zug/virtualenv-py3.13-PYTHONHASHSEED0]       |
|          | 3.21±0.4ms                 | 3.10±0.07ms                                    |    0.97 | specifiers.TimeSpecSuite.time_contains_cold [zug/virtualenv-py3.14-PYTHONHASHSEED0]       |
|          | 6.31±0.1ms                 | 6.29±0.7ms                                     |    1    | specifiers.TimeSpecSuite.time_contains_cold [zug/virtualenv-py3.8-PYTHONHASHSEED0]        |
|          | 6.16±0.8ms                 | 6.11±0.6ms                                     |    0.99 | specifiers.TimeSpecSuite.time_contains_cold [zug/virtualenv-py3.9-PYTHONHASHSEED0]        |
|          | 4.01±0.5ms                 | 3.93±0.08ms                                    |    0.98 | specifiers.TimeSpecSuite.time_contains_warm [zug/virtualenv-py3.10-PYTHONHASHSEED0]       |
|          | 3.09±0.3ms                 | 3.04±0.06ms                                    |    0.98 | specifiers.TimeSpecSuite.time_contains_warm [zug/virtualenv-py3.11-PYTHONHASHSEED0]       |
|          | 3.17±0.4ms                 | 3.17±0.1ms                                     |    1    | specifiers.TimeSpecSuite.time_contains_warm [zug/virtualenv-py3.12-PYTHONHASHSEED0]       |
|          | 3.11±0.4ms                 | 3.07±0.07ms                                    |    0.99 | specifiers.TimeSpecSuite.time_contains_warm [zug/virtualenv-py3.13-PYTHONHASHSEED0]       |
|          | 2.42±0.3ms                 | 2.35±0.04ms                                    |    0.97 | specifiers.TimeSpecSuite.time_contains_warm [zug/virtualenv-py3.14-PYTHONHASHSEED0]       |
|          | 4.76±0.2ms                 | 4.72±0.5ms                                     |    0.99 | specifiers.TimeSpecSuite.time_contains_warm [zug/virtualenv-py3.8-PYTHONHASHSEED0]        |
|          | 4.59±0.6ms                 | 4.60±0.5ms                                     |    1    | specifiers.TimeSpecSuite.time_contains_warm [zug/virtualenv-py3.9-PYTHONHASHSEED0]        |
|          | 12.0±1μs                   | 11.8±0.4μs                                     |    0.99 | specifiers.TimeSpecSuite.time_filter_simple_cold [zug/virtualenv-py3.10-PYTHONHASHSEED0]  |
|          | 8.04±0.9μs                 | 7.94±0.2μs                                     |    0.99 | specifiers.TimeSpecSuite.time_filter_simple_cold [zug/virtualenv-py3.11-PYTHONHASHSEED0]  |
|          | 7.53±0.9μs                 | 7.46±0.2μs                                     |    0.99 | specifiers.TimeSpecSuite.time_filter_simple_cold [zug/virtualenv-py3.12-PYTHONHASHSEED0]  |
|          | 7.85±1μs                   | 7.71±0.2μs                                     |    0.98 | specifiers.TimeSpecSuite.time_filter_simple_cold [zug/virtualenv-py3.13-PYTHONHASHSEED0]  |
|          | 6.12±0.7μs                 | 5.96±0.1μs                                     |    0.97 | specifiers.TimeSpecSuite.time_filter_simple_cold [zug/virtualenv-py3.14-PYTHONHASHSEED0]  |
|          | 14.0±0.4μs                 | 13.8±2μs                                       |    0.99 | specifiers.TimeSpecSuite.time_filter_simple_cold [zug/virtualenv-py3.8-PYTHONHASHSEED0]   |
|          | 13.4±2μs                   | 13.4±2μs                                       |    1.01 | specifiers.TimeSpecSuite.time_filter_simple_cold [zug/virtualenv-py3.9-PYTHONHASHSEED0]   |
|          | 10.2±1μs                   | 10.1±0.3μs                                     |    0.99 | specifiers.TimeSpecSuite.time_filter_simple_warm [zug/virtualenv-py3.10-PYTHONHASHSEED0]  |
|          | 6.94±0.7μs                 | 6.93±0.3μs                                     |    1    | specifiers.TimeSpecSuite.time_filter_simple_warm [zug/virtualenv-py3.11-PYTHONHASHSEED0]  |
|          | 6.36±0.8μs                 | 6.30±0.2μs                                     |    0.99 | specifiers.TimeSpecSuite.time_filter_simple_warm [zug/virtualenv-py3.12-PYTHONHASHSEED0]  |
|          | 6.82±0.8μs                 | 6.59±0.1μs                                     |    0.97 | specifiers.TimeSpecSuite.time_filter_simple_warm [zug/virtualenv-py3.13-PYTHONHASHSEED0]  |
|          | 5.01±0.6μs                 | 4.93±0.1μs                                     |    0.98 | specifiers.TimeSpecSuite.time_filter_simple_warm [zug/virtualenv-py3.14-PYTHONHASHSEED0]  |
|          | 11.9±0.2μs                 | 11.6±1μs                                       |    0.98 | specifiers.TimeSpecSuite.time_filter_simple_warm [zug/virtualenv-py3.8-PYTHONHASHSEED0]   |
|          | 11.5±1μs                   | 11.4±1μs                                       |    1    | specifiers.TimeSpecSuite.time_filter_simple_warm [zug/virtualenv-py3.9-PYTHONHASHSEED0]   |
|          | 3.14±0.4μs                 | 3.17±0.1μs                                     |    1.01 | utils.TimeUtils.time_canonicalize_name [zug/virtualenv-py3.10-PYTHONHASHSEED0]            |
|          | 2.48±0.3μs                 | 2.46±0.04μs                                    |    0.99 | utils.TimeUtils.time_canonicalize_name [zug/virtualenv-py3.11-PYTHONHASHSEED0]            |
|          | 2.66±0.3μs                 | 2.58±0.08μs                                    |    0.97 | utils.TimeUtils.time_canonicalize_name [zug/virtualenv-py3.12-PYTHONHASHSEED0]            |
|          | 2.80±0.4μs                 | 2.74±0.06μs                                    |    0.98 | utils.TimeUtils.time_canonicalize_name [zug/virtualenv-py3.13-PYTHONHASHSEED0]            |
|          | 2.43±0.3μs                 | 2.36±0.05μs                                    |    0.97 | utils.TimeUtils.time_canonicalize_name [zug/virtualenv-py3.14-PYTHONHASHSEED0]            |
|          | 3.97±0.1μs                 | 4.06±0.5μs                                     |    1.02 | utils.TimeUtils.time_canonicalize_name [zug/virtualenv-py3.8-PYTHONHASHSEED0]             |
|          | 3.83±0.4μs                 | 3.79±0.4μs                                     |    0.99 | utils.TimeUtils.time_canonicalize_name [zug/virtualenv-py3.9-PYTHONHASHSEED0]             |
|          | 1.71±0.2ms                 | 1.68±0.04ms                                    |    0.98 | version.TimeVersionSuite.time_constructor [zug/virtualenv-py3.10-PYTHONHASHSEED0]         |
|          | 1.10±0.1ms                 | 1.08±0.02ms                                    |    0.99 | version.TimeVersionSuite.time_constructor [zug/virtualenv-py3.11-PYTHONHASHSEED0]         |
|          | 1.33±0.2ms                 | 1.32±0.03ms                                    |    0.99 | version.TimeVersionSuite.time_constructor [zug/virtualenv-py3.12-PYTHONHASHSEED0]         |
|          | 1.26±0.1ms                 | 1.21±0.03ms                                    |    0.96 | version.TimeVersionSuite.time_constructor [zug/virtualenv-py3.13-PYTHONHASHSEED0]         |
|          | 1.04±0.1ms                 | 1.03±0.02ms                                    |    0.99 | version.TimeVersionSuite.time_constructor [zug/virtualenv-py3.14-PYTHONHASHSEED0]         |
|          | 2.06±0.04ms                | 2.06±0.2ms                                     |    1    | version.TimeVersionSuite.time_constructor [zug/virtualenv-py3.8-PYTHONHASHSEED0]          |
|          | 1.99±0.3ms                 | 2.02±0.2ms                                     |    1.01 | version.TimeVersionSuite.time_constructor [zug/virtualenv-py3.9-PYTHONHASHSEED0]          |
|          | 2.92±0.3ms                 | 2.89±0.06ms                                    |    0.99 | version.TimeVersionSuite.time_hash [zug/virtualenv-py3.10-PYTHONHASHSEED0]                |
|          | 1.98±0.2ms                 | 1.96±0.04ms                                    |    0.99 | version.TimeVersionSuite.time_hash [zug/virtualenv-py3.11-PYTHONHASHSEED0]                |
|          | 2.37±0.3ms                 | 2.31±0.03ms                                    |    0.98 | version.TimeVersionSuite.time_hash [zug/virtualenv-py3.12-PYTHONHASHSEED0]                |
|          | 2.33±0.3ms                 | 2.24±0.08ms                                    |    0.96 | version.TimeVersionSuite.time_hash [zug/virtualenv-py3.13-PYTHONHASHSEED0]                |
|          | 1.87±0.2ms                 | 1.82±0.03ms                                    |    0.98 | version.TimeVersionSuite.time_hash [zug/virtualenv-py3.14-PYTHONHASHSEED0]                |
|          | 3.49±0.06ms                | 3.45±0.4ms                                     |    0.99 | version.TimeVersionSuite.time_hash [zug/virtualenv-py3.8-PYTHONHASHSEED0]                 |
|          | 3.39±0.4ms                 | 3.38±0.3ms                                     |    1    | version.TimeVersionSuite.time_hash [zug/virtualenv-py3.9-PYTHONHASHSEED0]                 |
|          | 1.87±0.2ms                 | 1.85±0.04ms                                    |    0.99 | version.TimeVersionSuite.time_sort_cold [zug/virtualenv-py3.10-PYTHONHASHSEED0]           |
|          | 1.26±0.1ms                 | 1.24±0.03ms                                    |    0.98 | version.TimeVersionSuite.time_sort_cold [zug/virtualenv-py3.11-PYTHONHASHSEED0]           |
|          | 1.52±0.2ms                 | 1.51±0.02ms                                    |    0.99 | version.TimeVersionSuite.time_sort_cold [zug/virtualenv-py3.12-PYTHONHASHSEED0]           |
|          | 1.61±0.2ms                 | 1.57±0.04ms                                    |    0.98 | version.TimeVersionSuite.time_sort_cold [zug/virtualenv-py3.13-PYTHONHASHSEED0]           |
|          | 1.31±0.2ms                 | 1.28±0.02ms                                    |    0.98 | version.TimeVersionSuite.time_sort_cold [zug/virtualenv-py3.14-PYTHONHASHSEED0]           |
|          | 2.56±0.06ms                | 2.54±0.3ms                                     |    0.99 | version.TimeVersionSuite.time_sort_cold [zug/virtualenv-py3.8-PYTHONHASHSEED0]            |
|          | 2.47±0.3ms                 | 2.47±0.3ms                                     |    1    | version.TimeVersionSuite.time_sort_cold [zug/virtualenv-py3.9-PYTHONHASHSEED0]            |
|          | 1.33±0.2ms                 | 1.31±0.03ms                                    |    0.99 | version.TimeVersionSuite.time_sort_warm [zug/virtualenv-py3.10-PYTHONHASHSEED0]           |
|          | 1000±100μs                 | 994±20μs                                       |    0.99 | version.TimeVersionSuite.time_sort_warm [zug/virtualenv-py3.11-PYTHONHASHSEED0]           |
|          | 1.26±0.1ms                 | 1.23±0.02ms                                    |    0.98 | version.TimeVersionSuite.time_sort_warm [zug/virtualenv-py3.12-PYTHONHASHSEED0]           |
|          | 1.32±0.2ms                 | 1.27±0.03ms                                    |    0.97 | version.TimeVersionSuite.time_sort_warm [zug/virtualenv-py3.13-PYTHONHASHSEED0]           |
|          | 1.05±0.1ms                 | 1.03±0.03ms                                    |    0.98 | version.TimeVersionSuite.time_sort_warm [zug/virtualenv-py3.14-PYTHONHASHSEED0]           |
|          | 2.03±0.05ms                | 2.02±0.2ms                                     |    0.99 | version.TimeVersionSuite.time_sort_warm [zug/virtualenv-py3.8-PYTHONHASHSEED0]            |
|          | 1.92±0.2ms                 | 1.92±0.2ms                                     |    1    | version.TimeVersionSuite.time_sort_warm [zug/virtualenv-py3.9-PYTHONHASHSEED0]            |
|          | 2.77±0.3ms                 | 2.71±0.05ms                                    |    0.98 | version.TimeVersionSuite.time_str [zug/virtualenv-py3.10-PYTHONHASHSEED0]                 |
|          | 1.94±0.2ms                 | 1.90±0.03ms                                    |    0.98 | version.TimeVersionSuite.time_str [zug/virtualenv-py3.11-PYTHONHASHSEED0]                 |
|          | 2.16±0.3ms                 | 2.14±0.03ms                                    |    0.99 | version.TimeVersionSuite.time_str [zug/virtualenv-py3.12-PYTHONHASHSEED0]                 |
|          | 2.02±0.3ms                 | 1.97±0.06ms                                    |    0.98 | version.TimeVersionSuite.time_str [zug/virtualenv-py3.13-PYTHONHASHSEED0]                 |
|          | 1.70±0.2ms                 | 1.67±0.04ms                                    |    0.98 | version.TimeVersionSuite.time_str [zug/virtualenv-py3.14-PYTHONHASHSEED0]                 |
|          | 3.51±0.07ms                | 3.52±0.3ms                                     |    1    | version.TimeVersionSuite.time_str [zug/virtualenv-py3.8-PYTHONHASHSEED0]                  |
|          | 3.41±0.4ms                 | 3.35±0.4ms                                     |    0.98 | version.TimeVersionSuite.time_str [zug/virtualenv-py3.9-PYTHONHASHSEED0]                  |

This has been great for me in terms of playing with asv, but I'm not sure what needs to happen with the PR. I'd appreciate feedback/guidance about what seems like a reasonable next step.

@notatallshaw
Copy link
Copy Markdown
Member

notatallshaw commented Mar 8, 2026

Looking at the latest merge with master (which did affect the benchmarks) and rearranging in order of Python version so it's easier to make sense of:

|          | 1.97±0.01ms          | 2.03±0.01ms         |    1.03 | specifiers.TimeSpecSuite.time_constructor [runnervm0kj6c/virtualenv-py3.8-PYTHONHASHSEED0]          |
|          | 1.91±0.01ms          | 2.02±0.01ms         |    1.05 | specifiers.TimeSpecSuite.time_constructor [runnervm0kj6c/virtualenv-py3.9-PYTHONHASHSEED0]          |
|          | 1.94±0.01ms          | 2.04±0.01ms         |    1.05 | specifiers.TimeSpecSuite.time_constructor [runnervm0kj6c/virtualenv-py3.10-PYTHONHASHSEED0]         |
|          | 1.72±0.01ms          | 1.75±0.01ms         |    1.02 | specifiers.TimeSpecSuite.time_constructor [runnervm0kj6c/virtualenv-py3.11-PYTHONHASHSEED0]         |
|          | 1.91±0.02ms          | 1.93±0.02ms         |    1.01 | specifiers.TimeSpecSuite.time_constructor [runnervm0kj6c/virtualenv-py3.12-PYTHONHASHSEED0]         |
|          | 1.86±0.01ms          | 1.74±0.01ms         |    0.93 | specifiers.TimeSpecSuite.time_constructor [runnervm0kj6c/virtualenv-py3.13-PYTHONHASHSEED0]         |
|          | 1.81±0.01ms          | 1.72±0.01ms         |    0.95 | specifiers.TimeSpecSuite.time_constructor [runnervm0kj6c/virtualenv-py3.14-PYTHONHASHSEED0]         |

I could convince myself that it's a hair slower on Python 3.10 and below and a hair faster on Python 3.13+, but it's tough.

But I'm personally terrified of regexes, so I'm going to deffer to @henryiii on if the regex itself is better (for some definition of the sense better, such as more maintainable).

@henryiii
Copy link
Copy Markdown
Contributor

henryiii commented Mar 8, 2026

I ran it locally:

Change Before [9b97201] After [8c886c3] Ratio Benchmark (Parameter)
8.16±0.03ms 8.01±0.04ms 0.98 requirement.TimeRequirementSuite.time_constructor [PU-H2WF61JRQ6NY/virtualenv-py3.10-PYTHONHASHSEED0]
6.03±0.07ms 5.87±0.07ms 0.97 requirement.TimeRequirementSuite.time_constructor [PU-H2WF61JRQ6NY/virtualenv-py3.11-PYTHONHASHSEED0]
6.79±0.01ms 6.65±0.02ms 0.98 requirement.TimeRequirementSuite.time_constructor [PU-H2WF61JRQ6NY/virtualenv-py3.12-PYTHONHASHSEED0]
6.01±0.03ms 5.85±0.02ms 0.97 requirement.TimeRequirementSuite.time_constructor [PU-H2WF61JRQ6NY/virtualenv-py3.13-PYTHONHASHSEED0]
6.21±0.01ms 6.02±0.01ms 0.97 requirement.TimeRequirementSuite.time_constructor [PU-H2WF61JRQ6NY/virtualenv-py3.14-PYTHONHASHSEED0]
8.67±0.02ms 8.60±0.01ms 0.99 requirement.TimeRequirementSuite.time_constructor [PU-H2WF61JRQ6NY/virtualenv-py3.8-PYTHONHASHSEED0]
8.57±0.08ms 8.41±0.06ms 0.98 requirement.TimeRequirementSuite.time_constructor [PU-H2WF61JRQ6NY/virtualenv-py3.9-PYTHONHASHSEED0]
1.15±0.01ms 1.13±0ms 0.98 specifiers.TimeSpecSuite.time_constructor [PU-H2WF61JRQ6NY/virtualenv-py3.10-PYTHONHASHSEED0]
1.00±0ms 945±5μs 0.94 specifiers.TimeSpecSuite.time_constructor [PU-H2WF61JRQ6NY/virtualenv-py3.11-PYTHONHASHSEED0]
1.12±0.01ms 1.06±0.01ms 0.94 specifiers.TimeSpecSuite.time_constructor [PU-H2WF61JRQ6NY/virtualenv-py3.12-PYTHONHASHSEED0]
- 1.06±0.01ms 938±5μs 0.88 specifiers.TimeSpecSuite.time_constructor [PU-H2WF61JRQ6NY/virtualenv-py3.13-PYTHONHASHSEED0]
- 1.06±0.01ms 948±20μs 0.89 specifiers.TimeSpecSuite.time_constructor [PU-H2WF61JRQ6NY/virtualenv-py3.14-PYTHONHASHSEED0]
1.12±0ms 1.10±0ms 0.99 specifiers.TimeSpecSuite.time_constructor [PU-H2WF61JRQ6NY/virtualenv-py3.8-PYTHONHASHSEED0]
1.11±0ms 1.09±0ms 0.98 specifiers.TimeSpecSuite.time_constructor [PU-H2WF61JRQ6NY/virtualenv-py3.9-PYTHONHASHSEED0]

I'd say that's an improvement, even before 3.13 (but mostly after).

Copy link
Copy Markdown
Contributor

@henryiii henryiii left a comment

Choose a reason for hiding this comment

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

IMO, this seems easier to reason about.

@sirosen sirosen marked this pull request as ready for review March 8, 2026 06:30
@sirosen
Copy link
Copy Markdown
Contributor Author

sirosen commented Mar 8, 2026

I think I want to move that .strip() call, but that shouldn't impact the benchmarking or impact readability of the more significant regex changes.

I'm double checking the benchmarks now (I realized moving it might have surprising impact).


EDIT: Actually, I went back and decided against it, although my testing shows it has no perf impact.

Here's the diff I had in mind:

         if spec.startswith("==="):
-            operator, version = spec[:3], spec[3:].strip()
+            operator, version = spec[:3], spec[3:]
         elif spec.startswith(("~=", "==", "!=", "<=", ">=")):
-            operator, version = spec[:2], spec[2:].strip()
+            operator, version = spec[:2], spec[2:]
         else:
-            operator, version = spec[:1], spec[1:].strip()
+            operator, version = spec[:1], spec[1:]
 
-        self._spec: tuple[str, str] = (operator, version)
+        self._spec: tuple[str, str] = (operator, version.strip())

This seemed like "simple cleanup", but I didn't really like that it assigns to version something other than what goes into self._spec[1].

I felt like this is a neutral change, not really a big improvement to readability but also not harmful. If someone else feels strongly that the version with only one call to strip() is much better, I can change it.

@henryiii
Copy link
Copy Markdown
Contributor

henryiii commented Mar 8, 2026

I like the individual strips a little better. But pretty neutral.

Examining the specifier regex, two potential optimizations seem
worthwhile:

1. Negative lookbehinds can be eliminated if we combine the operator
   regex with the version regex -- this avoids crawling the same
   characters of a string forwards and backwards.

2. Do more string operations, like `strip()` rather than having the
   regex engine do this work.

These ideas are somewhat combined, in that getting rid of the lookbehinds
only really works by eliminating the group selectors in use. And removing
the group selectors in the regex requires that we do more string
operations.

This also interestingly results in a regex stored in the `Specifier`
which matches its secondary usage in `_tokenizer`, which was previously
very slightly misaligned (in that the group captures in the regex were
unused).
@henryiii henryiii force-pushed the micro-optimize-specifiers branch from 8c886c3 to 6a2a044 Compare March 9, 2026 02:32
@henryiii henryiii changed the title Simply Specifier regex for slightly better perf perf: simply Specifier regex Mar 9, 2026
@henryiii henryiii merged commit e839d73 into pypa:main Mar 9, 2026
56 checks passed
@sirosen sirosen deleted the micro-optimize-specifiers branch March 9, 2026 21:19
ngoldbaum pushed a commit to ngoldbaum/packaging that referenced this pull request Apr 1, 2026
Examining the specifier regex, two potential optimizations seem
worthwhile:

1. Negative lookbehinds can be eliminated if we combine the operator
   regex with the version regex -- this avoids crawling the same
   characters of a string forwards and backwards.

2. Do more string operations, like `strip()` rather than having the
   regex engine do this work.

These ideas are somewhat combined, in that getting rid of the lookbehinds
only really works by eliminating the group selectors in use. And removing
the group selectors in the regex requires that we do more string
operations.

This also interestingly results in a regex stored in the `Specifier`
which matches its secondary usage in `_tokenizer`, which was previously
very slightly misaligned (in that the group captures in the regex were
unused).
This was referenced Apr 13, 2026
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.

3 participants