uv/crates/pep440-rs
Andrew Gallant f9528b4ecd
pep440-rs: switch Version::release to smallvec
This commit attempts an optimization that switches a version's `release`
field over to a `smallvec` optimization. The idea is that most versions
are very small and can be stored inline.

Interestingly, I was unable to observe any obvious benefit:

    $ hyperfine \
        "./target/profiling/puffin-dev-u32 resolve-many --cache-dir cache-docker-no-build --no-build pypi_top_8k_flat.txt --limit 1000 2> /dev/null" \
        "./target/profiling/puffin-dev-smallvec-release resolve-many --cache-dir cache-docker-no-build --no-build pypi_top_8k_flat.txt --limit 1000 2> /dev/null"
    Benchmark 1: ./target/profiling/puffin-dev-u32 resolve-many --cache-dir cache-docker-no-build --no-build pypi_top_8k_flat.txt --limit 1000 2> /dev/null
      Time (mean ± σ):     872.2 ms ±  26.5 ms    [User: 14646.0 ms, System: 2516.0 ms]
      Range (min … max):   833.0 ms … 912.0 ms    10 runs

    Benchmark 2: ./target/profiling/puffin-dev-smallvec-release resolve-many --cache-dir cache-docker-no-build --no-build pypi_top_8k_flat.txt --limit 1000 2> /dev/null
      Time (mean ± σ):     882.3 ms ±  17.4 ms    [User: 14764.4 ms, System: 2520.9 ms]
      Range (min … max):   859.7 ms … 912.7 ms    10 runs

    Summary
      './target/profiling/puffin-dev-u32 resolve-many --cache-dir cache-docker-no-build --no-build pypi_top_8k_flat.txt --limit 1000 2> /dev/null' ran
        1.01 ± 0.04 times faster than './target/profiling/puffin-dev-smallvec-release resolve-many --cache-dir cache-docker-no-build --no-build pypi_top_8k_flat.txt --limit 1000 2> /dev/null'

My hypothesis is that because of an earlier commit that switched the
global allocator to jemalloc, the cost of allocation had precipitously
decreased. To the point that the reduction in allocs from the smallvec
becomes a wash. To test my hypothesis, I dropped the jemalloc commit and
measured the perf of the smallvec optimization against main:

    $ hyperfine \
        "./target/profiling/puffin-dev-main resolve-many --cache-dir cache-docker-no-build --no-build pypi_top_8k_flat.txt --limit 1000 2> /dev/null" \
        "./target/profiling/puffin-dev-smallvec-release-no-jemalloc resolve-many --cache-dir cache-docker-no-build --no-build pypi_top_8k_flat.txt --limit 1000 2> /dev/null"
    Benchmark 1: ./target/profiling/puffin-dev-main resolve-many --cache-dir cache-docker-no-build --no-build pypi_top_8k_flat.txt --limit 1000 2> /dev/null
      Time (mean ± σ):     968.0 ms ±  20.0 ms    [User: 17637.4 ms, System: 2151.9 ms]
      Range (min … max):   940.2 ms … 1005.3 ms    10 runs

    Benchmark 2: ./target/profiling/puffin-dev-smallvec-release-no-jemalloc resolve-many --cache-dir cache-docker-no-build --no-build pypi_top_8k_flat.txt --limit 1000 2> /dev/null
      Time (mean ± σ):     958.4 ms ±  15.7 ms    [User: 17119.7 ms, System: 2246.1 ms]
      Range (min … max):   944.7 ms … 993.3 ms    10 runs

    Summary
      './target/profiling/puffin-dev-smallvec-release-no-jemalloc resolve-many --cache-dir cache-docker-no-build --no-build pypi_top_8k_flat.txt --limit 1000 2> /dev/null' ran
        1.01 ± 0.03 times faster than './target/profiling/puffin-dev-main resolve-many --cache-dir cache-docker-no-build --no-build pypi_top_8k_flat.txt --limit 1000 2> /dev/null'

Fiddlesticks. Even when allocation is (presumably) more expensive, the
smallvec optimization didn't help. This suggests something is off about
my mental model of the code. So there are more avenues to explore here!
2023-11-10 14:30:26 -05:00
..
python Unify python interpreter abstractions (#178) 2023-10-25 20:11:36 +00:00
src pep440-rs: switch Version::release to smallvec 2023-11-10 14:30:26 -05:00
test Copy over `pep440-rs` crate (#30) 2023-10-06 20:11:52 -04:00
CHANGELOG.md Enable release builds via `cargo-dist` (#79) 2023-10-09 20:48:55 +00:00
Cargo.lock Copy over `pep440-rs` crate (#30) 2023-10-06 20:11:52 -04:00
Cargo.toml pep440-rs: switch Version::release to smallvec 2023-11-10 14:30:26 -05:00
License-Apache Copy over `pep440-rs` crate (#30) 2023-10-06 20:11:52 -04:00
License-BSD Copy over `pep440-rs` crate (#30) 2023-10-06 20:11:52 -04:00
README.md Enable release builds via `cargo-dist` (#79) 2023-10-09 20:48:55 +00:00

README.md

PEP440 in rust

Crates.io PyPI

A library for python version numbers and specifiers, implementing PEP 440. See Reimplementing PEP 440 for some background.

Higher level bindings to the requirements syntax are available in pep508_rs.

use std::str::FromStr;
use pep440_rs::{parse_version_specifiers, Version, VersionSpecifier};

let version = Version::from_str("1.19").unwrap();
let version_specifier = VersionSpecifier::from_str("==1.*").unwrap();
assert!(version_specifier.contains(&version));
let version_specifiers = parse_version_specifiers(">=1.16, <2.0").unwrap();
assert!(version_specifiers.iter().all(|specifier| specifier.contains(&version)));

In python (pip install pep440_rs):

from pep440_rs import Version, VersionSpecifier

assert Version("1.1a1").any_prerelease()
assert Version("1.1.dev2").any_prerelease()
assert not Version("1.1").any_prerelease()
assert VersionSpecifier(">=1.0").contains(Version("1.1a1"))
assert not VersionSpecifier(">=1.1").contains(Version("1.1a1"))
# Note that python comparisons are the version ordering, not the version specifiers operators
assert Version("1.1") >= Version("1.1a1")
assert Version("2.0") in VersionSpecifier("==2")

PEP 440 has a lot of unintuitive features, including:

  • An epoch that you can prefix the version which, e.g. 1!1.2.3. Lower epoch always means lower version (1.0 <=2!0.1)
  • post versions, which can be attached to both stable releases and prereleases
  • dev versions, which can be attached to sbpth table releases and prereleases. When attached to a prerelease the dev version is ordered just below the normal prerelease, however when attached to a stable version, the dev version is sorted before a prereleases
  • prerelease handling is a mess: "Pre-releases of any kind, including developmental releases, are implicitly excluded from all version specifiers, unless they are already present on the system, explicitly requested by the user, or if the only available version that satisfies the version specifier is a pre-release.". This means that we can't say whether a specifier matches without also looking at the environment
  • prelease vs. prerelease incl. dev is fuzzy
  • local versions on top of all the others, which are added with a + and have implicitly typed string and number segments
  • no semver-caret (^), but a pseudo-semver tilde (~=)
  • ordering contradicts matching: We have e.g. 1.0+local > 1.0 when sorting, but ==1.0 matches 1.0+local. While the ordering of versions itself is a total order the version matching needs to catch all sorts of special cases