Skip to content
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

Remove [u64; 4] from small version to move Arc to full version #10345

Merged
merged 3 commits into from
Jan 7, 2025

Conversation

konstin
Copy link
Member

@konstin konstin commented Jan 7, 2025

Ref #10344

Cloning and dropping the version arc took a significant fraction of the time in the resolver, which is a large overhead especially for the small variant that has only 9 bytes payload.

When moving the Arc to only apply to the full variant, the small variant is too large because it stores a [u64; 4] to have a release accessor a &[u64] that's shared with the Vec<u64> of the full variant. We proxy this by first extracting the compressed version digits of the small variant to a proxy type that stores up to 4 u64 on the stack and can be deref'ed to the existing &[u64], minimizing churn.

@konstin konstin requested a review from BurntSushi January 7, 2025 09:30
Cloning and dropping the version arc took a significant fraction of the time in the resolver, which is a large overhead especially for the small variant that has only 9 bytes payload.

When moving the `Arc` to only apply to the full variant, the small variant is too large because it stores a `[u64; 4]` to have a release accessor a `&[u64]` that's shared with the `Vec<u64>` of the full variant. We proxy this by first extracting the compressed version digits of the small variant to a proxy type that stores up to 4 u64 on the stack and can be deref'ed to the existing `&[u64]`, minimizing churn.

```
$ Benchmark 1: target/profiling/uv pip compile scripts/requirements/airflow.in
    Time (mean ± σ):     361.3 ms ±   2.7 ms    [User: 503.4 ms, System: 174.9 ms]
    Range (min … max):   356.3 ms … 365.2 ms    10 runs

  Benchmark 2: ./uv-3 pip compile scripts/requirements/airflow.in
    Time (mean ± σ):     402.9 ms ±   8.5 ms    [User: 571.2 ms, System: 196.6 ms]
    Range (min … max):   393.9 ms … 418.0 ms    10 runs

  Summary
    target/profiling/uv pip compile scripts/requirements/airflow.in ran
      1.12 ± 0.03 times faster than ./uv-3 pip compile scripts/requirements/airflow.in
```
@konstin konstin force-pushed the konsti/opt-small-version-arc branch from 4053db3 to 64a8980 Compare January 7, 2025 09:30
@konstin konstin added the performance Potential performance improvement label Jan 7, 2025
Copy link

codspeed-hq bot commented Jan 7, 2025

CodSpeed Performance Report

Merging #10345 will improve performances by 29.06%

Comparing konsti/opt-small-version-arc (c09adcf) with main (14a9008)

Summary

⚡ 3 improvements
✅ 11 untouched benchmarks

Benchmarks breakdown

Benchmark main konsti/opt-small-version-arc Change
resolve_warm_airflow 1,039.2 ms 841.5 ms +23.48%
resolve_warm_jupyter 95 ms 73.6 ms +29.06%
resolve_warm_jupyter_universal 232.7 ms 209.6 ms +11.02%

Copy link
Member

@BurntSushi BurntSushi left a comment

Choose a reason for hiding this comment

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

This is awesome! I love it.

@@ -1416,6 +1418,39 @@ impl FromStr for VersionPattern {
}
}

/// Lifetime and indexing workaround to allow accessing the release as `&[u64]` even though the
/// digits may be stored in a compressed representation.
Copy link
Member

Choose a reason for hiding this comment

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

Since this is part of the public API, I'd suggest keeping the first sentence to one line, and then adding more (if necessary) in a paragraph below it. Otherwise, this will show up as an overlong summary in the type listing.

Small3([u64; 3]),
Small4([u64; 4]),
Full(&'a [u64]),
}
Copy link
Member

Choose a reason for hiding this comment

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

I was wondering if there was a more compact representation than this, so I tried:

enum ReleaseInner<'a> {
    Small { numbers: [u64; 4], len: u8 },
    Full(&'a [u64]),
}

But indeed, it's the same size (which makes sense).

@@ -930,6 +939,8 @@ struct VersionSmall {
/// places somewhat exposes internal details, since the "full" version
/// representation would not do that.
len: u8,
Copy link
Member

Choose a reason for hiding this comment

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

Oh man, if we could get rid of len here, this type would decrease in size by half I believe. IDK if that would make an actual difference perf-wise... And I think the only way to do it would be to take bits away from repr, which in turn means moving more cases to the Full representation. Which might be a net negative overall.

Copy link
Member Author

Choose a reason for hiding this comment

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

We could get rid of len by counting the zeroes ourselves. The problem is that Version doesn't become 64 byte since the Arc has only a single niche (NonNull) in rustc's view, so both variants don't fit into a 64-bit word.

We could try a tagged pointer library, or we could lean into it and make the small representation [u8; 11] or [u8; 15] to use the space we're already allocating while leaving a bit (byte) for rustc for the enum discriminant.

This problem MRE:

use std::sync::Arc;

enum A {
    Small(()),
    Large(Arc<Vec<u8>>),
}

enum B {
    Small(bool),
    Large(Arc<Vec<u8>>),
}

enum C {
    Small([u8; 7]),
    Large(Arc<Vec<u8>>),
}

fn main() {
    println!("{}", size_of::<A>()); // -> 8
    println!("{}", size_of::<B>()); // -> 16, but can it be 8 please?
    println!("{}", size_of::<C>()); // -> 16, but can it be 8 please?
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Another option would be stealing two bits somewhere else (fourth release number, or capping the first release number to 14 bits).

Copy link
Member

Choose a reason for hiding this comment

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

We could get rid of len by counting the zeroes ourselves.

Hmmm, but then I think you might lose the current property that trailing zeros are preserved. See: #10362.

Otherwise yeah, I see. I agree that going further probably means a tagged pointer of some kind.

A less thought out idea is a global interner like we use in uv-pep508 for markers. But whether that hits the right trade-offs is not at all clear to me. It could let you get a Version down to 32-bits pretty easily I think, but then pretty much every interaction with it would require consulting the global interner table. And that will probably destroy any benefits you might otherwise get.

(small.repr >> 0o60) & 0xFFFF,
(small.repr >> 0o50) & 0xFF,
(small.repr >> 0o40) & 0xFF,
(small.repr >> 0o30) & 0xFF,
Copy link
Member

Choose a reason for hiding this comment

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

Octal!?!?! -_- >_< o_0 Haha

BurntSushi added a commit that referenced this pull request Jan 7, 2025
Basically, this explicitly checks that parsing a `1.2.0` into a
`Version` will roundtrip back to a `1.2.0`, and that parsing a `1.2`
will roundtrip back to a `1.2`.

I think this case is included in the other tests in this module, but
this test makes the behavior more clearly intentional I think.

Ref #10345
@konstin konstin enabled auto-merge (squash) January 7, 2025 14:15
BurntSushi added a commit that referenced this pull request Jan 7, 2025
Basically, this explicitly checks that parsing a `1.2.0` into a
`Version` will roundtrip back to a `1.2.0`, and that parsing a `1.2`
will roundtrip back to a `1.2`.

I think this case is included in the other tests in this module, but
this test makes the behavior more clearly intentional I think.

Ref #10345
@konstin konstin merged commit 373e34f into main Jan 7, 2025
64 checks passed
@konstin konstin deleted the konsti/opt-small-version-arc branch January 7, 2025 14:25
BurntSushi added a commit that referenced this pull request Jan 7, 2025
This happened as a result of #10345 and #10362 being merged
independently. The latter used the old `Version::release` API, but the
former changed the `Version::release` API. This PR tweaks the new test
to use the new API (i.e., force a deref on the proxy type).
BurntSushi added a commit that referenced this pull request Jan 7, 2025
This happened as a result of #10345 and #10362 being merged
independently. The latter used the old `Version::release` API, but the
former changed the `Version::release` API. This PR tweaks the new test
to use the new API (i.e., force a deref on the proxy type).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance Potential performance improvement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants