Skip to content

Commit 9adb63e

Browse files
Reduce VersionSpecifiers parsing allocations (#20105)
## Summary `VersionSpecifiers::from_str` currently collects every parsed constraint into a growable `Vec`, sorts it, and converts it to a boxed slice. Most `requires-python` values contain a single clause, so parsing a value like `>=3.8` pays for temporary spare capacity and sorting before shrinking the allocation. This constructs empty and single-clause collections directly, and counts separators once so multi-clause inputs reserve their exact final size. Parsing and error behavior remain unchanged. In profiling builds, the single-clause case improves from about 129 ns to 74 ns (-43%). Exact sizing improves a two-clause input by about 10% and a five-clause input by about 9%. A CodSpeed benchmark covers all three shapes. The `uv-pep440` and `uv-pypi-types` suites pass, along with formatting and strict Clippy checks for `uv-pep440` and the new benchmark.
1 parent 5c0c508 commit 9adb63e

2 files changed

Lines changed: 35 additions & 6 deletions

File tree

crates/uv-bench/benches/uv.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use uv_client::{BaseClientBuilder, Connectivity, RegistryClientBuilder};
2020
use uv_distribution_filename::{SourceDistExtension, WheelFilename};
2121
use uv_distribution_types::Requirement;
2222
use uv_install_wheel::{InstallState, Layout, LinkMode};
23+
use uv_pep440::VersionSpecifiers;
2324
use uv_preview::Preview;
2425
use uv_pypi_types::Scheme;
2526
use uv_python::PythonEnvironment;
@@ -308,6 +309,19 @@ fn resolve_warm_airflow(c: &mut Criterion<WallTime>) {
308309
c.bench_function("resolve_warm_airflow", |b| b.iter(&run));
309310
}
310311

312+
fn parse_version_specifiers(c: &mut Criterion<WallTime>) {
313+
let mut group = c.benchmark_group("parse_version_specifiers");
314+
for specifiers in [">=3.8", ">=3.8,<4", ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*, <4"] {
315+
group.bench_with_input(specifiers, specifiers, |benchmark, specifiers| {
316+
benchmark.iter(|| {
317+
VersionSpecifiers::from_str(black_box(specifiers))
318+
.expect("benchmark input should be valid")
319+
});
320+
});
321+
}
322+
group.finish();
323+
}
324+
311325
// This takes >5m to run in CodSpeed.
312326
// fn resolve_warm_airflow_universal(c: &mut Criterion<WallTime>) {
313327
// let manifest = Manifest::simple(vec![
@@ -328,7 +342,8 @@ criterion_group!(
328342
install_wheel_many_files,
329343
resolve_warm_jupyter,
330344
resolve_warm_jupyter_universal,
331-
resolve_warm_airflow
345+
resolve_warm_airflow,
346+
parse_version_specifiers
332347
);
333348
criterion_main!(uv);
334349

crates/uv-pep440/src/version_specifier.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,23 @@ impl FromStr for VersionSpecifiers {
156156
type Err = VersionSpecifiersParseError;
157157

158158
fn from_str(s: &str) -> Result<Self, Self::Err> {
159-
parse_version_specifiers(s).map(Self::from_unsorted)
159+
let separator_count = s.bytes().filter(|byte| *byte == b',').count();
160+
if separator_count == 0 {
161+
if s.is_empty() {
162+
return Ok(Self::empty());
163+
}
164+
return VersionSpecifier::from_str(s)
165+
.map(Self::from)
166+
.map_err(|err| VersionSpecifiersParseError {
167+
inner: Box::new(VersionSpecifiersParseErrorInner {
168+
err,
169+
line: s.to_string(),
170+
start: 0,
171+
end: s.len(),
172+
}),
173+
});
174+
}
175+
parse_version_specifiers(s, separator_count + 1).map(Self::from_unsorted)
160176
}
161177
}
162178

@@ -937,11 +953,9 @@ impl From<ParseErrorKind> for VersionSpecifierParseError {
937953
/// Parse a list of specifiers such as `>= 1.0, != 1.3.*, < 2.0`.
938954
fn parse_version_specifiers(
939955
spec: &str,
956+
specifier_count: usize,
940957
) -> Result<Vec<VersionSpecifier>, VersionSpecifiersParseError> {
941-
let mut version_ranges = Vec::new();
942-
if spec.is_empty() {
943-
return Ok(version_ranges);
944-
}
958+
let mut version_ranges = Vec::with_capacity(specifier_count);
945959
let mut start: usize = 0;
946960
let separator = ",";
947961
for version_range_spec in spec.split(separator) {

0 commit comments

Comments
 (0)