Repository navigation
Make Rc<T>::deref zero-cost - #141348
Make Rc<T>::deref zero-cost#141348EFanZh wants to merge 2 commits into
Rc<T>::deref zero-cost#141348Conversation
This comment has been minimized.
This comment has been minimized.
df34f84 to
d3a7429
Compare
This comment has been minimized.
This comment has been minimized.
bc84ec6 to
19fb34b
Compare
|
The Miri subtree was changed cc @rust-lang/miri |
19fb34b to
f5245ba
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment has been minimized.
This comment has been minimized.
This comment was marked as outdated.
This comment was marked as outdated.
Make `Rc<T>::deref` zero-cost This PR makes `Rc::deref` zero-cost by changing the internal pointer so that it points to the value directly instead of the allocation. This is split out from #132553, which will also make `Arc::deref` zero-cost.
This comment was marked as outdated.
This comment was marked as outdated.
This comment has been minimized.
This comment has been minimized.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
|
Hopefully rust-lang/rustc-perf#2615 will give some better hints about exactly where things are slowing down. |
There was a problem hiding this comment.
So you can run perf yourself and experiment,
@bors delegate=try
I know the patterns in the current code have been optimized about as much as possible, any kind of perturbation may easily have negative effects on performance. The best thing to do is probably to slowly walk back changes to make the code's shape look more like what's currently there. If that means moving more out of common.rs and back into rc.rs then that's fine, more refactoring can happen after the two big PRs merge.
| pub fn new_uninit() -> Rc<mem::MaybeUninit<T>> { | ||
| // ignore-tidy-undocumented-unsafe | ||
| unsafe { | ||
| Rc::from_ptr(Rc::allocate_for_layout( | ||
| Layout::new::<T>(), | ||
| |layout| Global.allocate(layout), | ||
| <*mut u8>::cast, | ||
| )) | ||
| } | ||
| Self::new_uninit_in(Global) | ||
| } |
There was a problem hiding this comment.
Could you try adding this pattern back with allocate_for_layout and try_allocate_for_layout? With API in common:
fn rc_inner_layout_for_value_layout(layout: Layout) -> Layout;
impl RcHeader {
unsafe fn allocate_for_layout(
value_layout: Layout,
allocate: impl FnOnce(Layout) -> Result<NonNull<[u8]>, AllocError>,
mem_to_rc_inner: impl FnOnce(ErasedRcValuePointer) -> RcValuePointer<T>,
) -> RcValuePointer<T>;
}I think we may be spending more time/complexity doing layout calculation and allocation while the existing pattern works quite well.
There was a problem hiding this comment.
I inspected the generated assembly locally, I think the majority difference is caused by different inlining strategy and extra pointer offset operations. Most of the test cases use sized types, I ensured the layouts are computed at compile time, so the layout calculation are not likely the main cause.
I am now trying different optimization strategies to see if they improve the perf result.
There was a problem hiding this comment.
I think we should keep this pattern anyway, actually. It's still easier to follow than what is currently in this PR, and we still want RcLayout to go away which this will help with.
At least we should have some better perf numbers before that with the added benchmarks.
|
✌️ @EFanZh, you can now perform try builds on this pull request! Warning You used the legacy format of the delegate command. You can now post |
This comment was marked as outdated.
This comment was marked as outdated.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment was marked as outdated.
This comment was marked as outdated.
This comment has been minimized.
This comment has been minimized.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
|
Ah can't work with the old build, @EFanZh could you rebase? |
This comment has been minimized.
This comment has been minimized.
|
@bors try @rust-timer queue |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Finished benchmarking commit (9e78396): comparison URL. Overall result: ❌✅ regressions and improvements - please read:Benchmarking means the PR may be perf-sensitive. It's automatically marked not fit for rolling up. Overriding is possible but disadvised: it risks changing compiler perf. Next, please: If you can, justify the regressions found in this try perf run in writing along with @bors rollup=never rustc-perf Instruction countOur most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.
Max RSS (memory usage)Results (primary -0.7%, secondary 2.9%)A less reliable metric. May be of interest, but not used to determine the overall result above.
CyclesResults (primary 2.6%, secondary 7.0%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Binary sizeResults (primary 0.1%, secondary 1.3%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Bootstrap: 489.662s -> 490.032s (0.08%) |
There was a problem hiding this comment.
Perf results at #141348 (comment) actually look pretty good, all things considered. There are some regressions for more niche issues, but the runtime benchmarks at https://perf.rust-lang.org/compare.html?start=56df4595f93c41b7d956612e6211f9a274cc6a5d&end=9e78396fdec429ba7badc774e4d66429ee890fd3&stat=instructions%3Au&tab=runtime&nonRelevant=true&runtimeName=rc_ are pretty positive.
It would be nice if we could shave off the 13% regression (maybe by getting some alloc computations back?) but don't think that's worth blocking on once we get the rest of this reviewed.
There was a problem hiding this comment.
@tgross35: the 13% regression is a debug build. I inspected it locally, and think it was mainly caused by debug assertions in various unchecked calls, I replaced them with the ones from the intrinsics mod, which skip debug assertions, and my local result shows improvement.
There was a problem hiding this comment.
Good idea for an experiment, we'll just have to remember to put it back once verifying that's where it comes from. (We shouldn't use intrinsics directly since the assertions in the unsafe wrappers are still useful in library API.)
There was a problem hiding this comment.
This test is a good idea, but could you put this into a separate PR? So we can see what this changes as a result of this one.
It would be good to also include an x86-only assembly test, covering most of the functions at https://rust.godbolt.org/z/TrbvGr4a8 for both Rc and Arc (excluding rc_new_slice because that's too big).
There was a problem hiding this comment.
@tgross35: Do you have a preference for the file layout for that PR? The current file have optimization in its name, I think the baseline probably need something else.
There was a problem hiding this comment.
The same naming is fine, as even without this PR we're checking how Rc and Arc optimize. Or just rc-arc
| #[inline(always)] | ||
| pub(super) fn try_allocate_uninit_for_type<A, T>( | ||
| alloc: &A, | ||
| ) -> Result<RcValuePointer<T>, AllocError> | ||
| where | ||
| A: Allocator, | ||
| { | ||
| Ok(try_allocate_uninit(alloc, RcLayout::new::<Self, T>())?.unerase()) | ||
| } | ||
|
|
||
| #[inline(always)] | ||
| pub(super) fn try_allocate_zeroed_for_type<A, T>( | ||
| alloc: &A, | ||
| ) -> Result<RcValuePointer<T>, AllocError> | ||
| where | ||
| A: Allocator, | ||
| { | ||
| Ok(try_allocate_zeroed(alloc, RcLayout::new::<Self, T>())?.unerase()) | ||
| } |
There was a problem hiding this comment.
Can you restore the previous allocate_for_layout pattern found at
rust/library/alloc/src/rcs/rc.rs
Lines 2375 to 2424 in 3e014ee
RcLayout, and goes a bit further to reduce the diff.
Those new functions can still live in common as methods on RcHeader so they can be reused by Arc.
There was a problem hiding this comment.
@tgross35: I can try restore allocate_for_layout to check the perf, but may I ask the reason behind removing RcLayout but keeping RcValuePointer? In my opinion, they play the same role of differentiating value properties from allocation properties.
There was a problem hiding this comment.
It's difficult to say, but the gist is that we felt RcLayout doesn't really do enough to be needed. It's only used in common and pretty much always constructed in a module-public function, then immediately passed to a module-private function that deconstructs it. The struct wrapper doesn't really prevent much more than calling a layout function, but it adds more indirection and type invariants to think about.
RcValuePointer OTOH is a smaller abstraction that winds up used more, e.g. passed through API surface across common, rc, and arc. It also gets stored in some structs where it would be easier to forget whether a NonNull<T> can access the header.
|
This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
@tgross35: My previous attempt of a perf run got rejected: #141348 (comment). Did I use the wrong command or do I also need the timer permission? |
|
@bors try @rust-timer queue |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
I guess those can't be delegated. You can ask at #t-compiler/help > perf run whenever you're ready for a perf run. |
This comment has been minimized.
This comment has been minimized.
|
Finished benchmarking commit (7384739): comparison URL. Overall result: ❌✅ regressions and improvements - please read:Benchmarking means the PR may be perf-sensitive. Consider adding rollup=never if this change is not fit for rolling up. Next, please: If you can, justify the regressions found in this try perf run in writing along with @bors rollup=iffy rustc-perf Instruction countOur most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.
Max RSS (memory usage)Results (primary 0.1%)A less reliable metric. May be of interest, but not used to determine the overall result above.
CyclesResults (secondary 3.8%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Binary sizeResults (primary 0.2%, secondary -0.9%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Bootstrap: 493.578s -> 488.646s (-1.00%) |
View all comments
This PR makes
Rc::derefzero-cost by changing the internal pointer to point directly to the value instead of to the allocation.This PR is split from #132553, which will also make
Arc::derefzero-cost.Review status:
RefCountsandRcLayouttypesRefCountertraitRawWeaktypeRawWeakmethods for sized valuesRawWeakmethods for slice valuesRawWeakRawRctypeRawRcmethods for sized valuesRawRcmethods forMaybeUninit<T>valuesRawRcmethods for slice valuesRawRcmethods fordyn AnytypeRawRcRawUniqueRctypeRawUniqueRcmethods for sized valuesRawUniqueRcalloc::rc::{Rc,Weak,UniqueRc}withalloc::raw_rctypesRcimplementation