-
Notifications
You must be signed in to change notification settings - Fork 959
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
Conversation
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 ```
4053db3
to
64a8980
Compare
CodSpeed Performance ReportMerging #10345 will improve performances by 29.06%Comparing Summary
Benchmarks breakdown
|
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.
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. |
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.
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]), | ||
} |
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 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, |
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.
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.
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.
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?
}
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.
Another option would be stealing two bits somewhere else (fourth release number, or capping the first release number to 14 bits).
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.
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, |
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.
Octal!?!?! -_- >_< o_0 Haha
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
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
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 theVec<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.