-
Notifications
You must be signed in to change notification settings - Fork 13.2k
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
stop specializing on Copy
#135634
base: master
Are you sure you want to change the base?
stop specializing on Copy
#135634
Conversation
rustbot has assigned @Mark-Simulacrum. Use |
Changes to the code generated for builtin derived traits. cc @nnethercote |
This comment has been minimized.
This comment has been minimized.
Going to nominate for libs-api (and libs) since this is both a breaking change (allowed since fixing soundness). I feel like I recall an RFC or some other discussion about us explicitly saying libraries shouldn't do the unsound thing here, but I don't know what that was. https://rust-lang.github.io/rfcs/1521-copy-clone-semantics.html is a bit related but not directly :) |
RFC 1521 could be interpreted so. Since it requires that Clone is equivalent to Copy when both are implemented. Since
I still don't think this is unsound in itself. So far all demonstrations of unsoundness required some other Noratrieb also argues that lifetime-conditional Copy currently is unsupported in MIR. So ISTM that this could be a documentation shortcoming and a compiler/lang issue that such implementations should be prevented but aren't. That said, I agree that the current situation is brittle. |
Without saying anything about specialization on Copy, there's definitely been past land discussion of splitting the "memcpyable" part of But that gets back to needing, as the8472 said, a way to actually block lifetime-bad implementations before it could be stable. |
We discussed this during today's libs-API meeting. We currently are not aware of any safe code that is unsound due to these specializations and there were concerns about performance regressions for user types that manually implement So we're leaning towards keeping the implementations as they are and instead improving things in other ways such as adding compiler warnings or improving the Copy documentation or unsafe-code-guidelines. We'd like input from T-types whether they agree with this assessment and if something should be changed on the language side, e.g. by forbidding or at least warning on lifetime-conditional implementations, similar to how A compiler-team member has indicated that lifetime-dependent Copy impls are de-facto unsupported. |
Forbidding lifetime dependent copy impls seems like it would be rather breaking (but that's pure speculation, we ought to do a crater run to check if anyone feels strongly we should forbid such impls), though generally I don't feel great about forbidding lifetime dependent copy impls. I also don't think a warning on lifetime dependent copy impls really helps anything for std as warnings cannot be relied upon for soundness and so std's usage of specialization would still be wrong. In general I would prefer std to not be using specialization in any ways that affect behaviour in any way, it's stably exposing unstable broken parts of the type system in ways that are arguably unsound (allows you to prove trait bounds hold when they do not). imo what should have happened is that years ago when specialization was found to be unsound all these specializations should have been ripped out regardless of the performance cost and re-added with a PR like this that respects lifetime constraints and treats the unsafe specialization marker attr as something unsafe with invariants to be upheld. I cant speak for the whole types team but that's atleast my opinion as a types member 🤷♀️ On a semi-related note, does std still specialize fused iterator stuff in ways that exposes specialization to stable too? I remember that being a thing some years ago but haven't kept up to date with how std is using specialization |
Yes, but #86765 changed the specialization so that incorrect specializations only result in correctness issues and not soundness ones. And we have |
The types team discussed this on zulip: https://rust-lang.zulipchat.com/#narrow/channel/326866-t-types.2Fnominated/topic/.23135634.3A.20stop.20specializing.20on.20.60Copy.60 My opinion/summary from there:
I would like to avoid specializing on |
☔ The latest upstream changes (presumably #136448) made this pull request unmergeable. Please resolve the merge conflicts. |
Should we add manual conditional impls for types like I know we're not going to perfectly recover everything that |
…]` on `Copy`, introduce `TrivialClone`
ff45e47
to
f2d28fe
Compare
This comment has been minimized.
This comment has been minimized.
Maybe, but let's just try the performance of this first: |
This comment has been minimized.
This comment has been minimized.
stop specializing on `Copy` fixes rust-lang#132442 `std` specializes on `Copy` to optimize certain library functions such as `clone_from_slice`. This is unsound, however, as the `Copy` implementation may not be always applicable because of lifetime bounds, which specialization does not take into account; the result being that values are copied even though they are not `Copy`. For instance, this code: ```rust struct SometimesCopy<'a>(&'a Cell<bool>); impl<'a> Clone for SometimesCopy<'a> { fn clone(&self) -> Self { self.0.set(true); Self(self.0) } } impl Copy for SometimesCopy<'static> {} let clone_called = Cell::new(false); // As SometimesCopy<'clone_called> is not 'static, this must run `clone`, // setting the value to `true`. let _ = [SometimesCopy(&clone_called)].clone(); assert!(clone_called.get()); ``` should not panic, but does ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=6be7a48cad849d8bd064491616fdb43c)). To solve this, this PR introduces a new `unsafe` trait: `TrivialClone`. This trait may be implemented whenever the `Clone` implementation is equivalent to copying the value (so e.g. `fn clone(&self) -> Self { *self }`). Because of lifetime erasure, there is no way for the `Clone` implementation to observe lifetime bounds, meaning that even if the `TrivialClone` has stricter bounds than the `Clone` implementation, its invariant still holds. Therefore, it is sound to specialize on `TrivialClone`. I've changed all `Copy` specializations in the standard library to specialize on `TrivialClone` instead. Unfortunately, the unsound `#[rustc_unsafe_specialization_marker]` attribute on `Copy` cannot be removed in this PR as `hashbrown` still depends on it. I'll make a PR updating `hashbrown` once this lands. With `Copy` no longer being considered for specialization, this change alone would result in the standard library optimizations not being applied for user types unaware of `TrivialClone`. To avoid this and restore the optimizations in most cases, I have changed the expansion of `#[derive(Clone)]`: Currently, whenever both `Clone` and `Copy` are derived, the `clone` method performs a copy of the value. With this PR, the derive macro also adds a `TrivialClone` implementation to make this case observable using specialization. I anticipate that most users will use `#[derive(Clone, Copy)]` whenever both are applicable, so most users will still profit from the library optimizations. Unfortunately, Hyrum's law applies to this PR: there are some popular crates which rely on the precise specialization behaviour of `core` to implement "specialization at home", e.g. [`libAFL`](https://github.com/AFLplusplus/LibAFL/blob/89cff637025c1652c24e8d97a30a2e3d01f187a4/libafl_bolts/src/tuples.rs#L27-L49). I have no remorse for breaking such horrible code, but perhaps we should open other, better ways to satisfy their needs – for example by dropping the `'static` bound on `TypeId::of`...
☀️ Try build successful - checks-actions |
This comment has been minimized.
This comment has been minimized.
Finished benchmarking commit (b50f56e): comparison URL. Overall result: ❌ regressions - please read the text belowBenchmarking this pull request likely means that it is perf-sensitive, so we're automatically marking it as not fit for rolling up. While you can manually mark this PR as fit for rollup, we strongly recommend not doing so since this PR may lead to changes in compiler perf. Next Steps: If you can justify the regressions found in this try perf run, please indicate this with @bors rollup=never Instruction countThis is the most reliable metric that we have; it was used to determine the overall result at the top of this comment. However, even this metric can sometimes exhibit noise.
Max RSS (memory usage)Results (primary 1.2%, secondary 2.5%)This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.
CyclesResults (primary -11.3%, secondary 1.6%)This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.
Binary sizeResults (primary 0.3%, secondary 0.3%)This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.
Bootstrap: 787s -> 787.644s (0.08%) |
Wow that's a lot better than I was expected. Great news |
Let's try if those two make up for the loss in performance. |
This comment has been minimized.
This comment has been minimized.
stop specializing on `Copy` fixes rust-lang#132442 `std` specializes on `Copy` to optimize certain library functions such as `clone_from_slice`. This is unsound, however, as the `Copy` implementation may not be always applicable because of lifetime bounds, which specialization does not take into account; the result being that values are copied even though they are not `Copy`. For instance, this code: ```rust struct SometimesCopy<'a>(&'a Cell<bool>); impl<'a> Clone for SometimesCopy<'a> { fn clone(&self) -> Self { self.0.set(true); Self(self.0) } } impl Copy for SometimesCopy<'static> {} let clone_called = Cell::new(false); // As SometimesCopy<'clone_called> is not 'static, this must run `clone`, // setting the value to `true`. let _ = [SometimesCopy(&clone_called)].clone(); assert!(clone_called.get()); ``` should not panic, but does ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=6be7a48cad849d8bd064491616fdb43c)). To solve this, this PR introduces a new `unsafe` trait: `TrivialClone`. This trait may be implemented whenever the `Clone` implementation is equivalent to copying the value (so e.g. `fn clone(&self) -> Self { *self }`). Because of lifetime erasure, there is no way for the `Clone` implementation to observe lifetime bounds, meaning that even if the `TrivialClone` has stricter bounds than the `Clone` implementation, its invariant still holds. Therefore, it is sound to specialize on `TrivialClone`. I've changed all `Copy` specializations in the standard library to specialize on `TrivialClone` instead. Unfortunately, the unsound `#[rustc_unsafe_specialization_marker]` attribute on `Copy` cannot be removed in this PR as `hashbrown` still depends on it. I'll make a PR updating `hashbrown` once this lands. With `Copy` no longer being considered for specialization, this change alone would result in the standard library optimizations not being applied for user types unaware of `TrivialClone`. To avoid this and restore the optimizations in most cases, I have changed the expansion of `#[derive(Clone)]`: Currently, whenever both `Clone` and `Copy` are derived, the `clone` method performs a copy of the value. With this PR, the derive macro also adds a `TrivialClone` implementation to make this case observable using specialization. I anticipate that most users will use `#[derive(Clone, Copy)]` whenever both are applicable, so most users will still profit from the library optimizations. Unfortunately, Hyrum's law applies to this PR: there are some popular crates which rely on the precise specialization behaviour of `core` to implement "specialization at home", e.g. [`libAFL`](https://github.com/AFLplusplus/LibAFL/blob/89cff637025c1652c24e8d97a30a2e3d01f187a4/libafl_bolts/src/tuples.rs#L27-L49). I have no remorse for breaking such horrible code, but perhaps we should open other, better ways to satisfy their needs – for example by dropping the `'static` bound on `TypeId::of`...
☀️ Try build successful - checks-actions |
This comment has been minimized.
This comment has been minimized.
Finished benchmarking commit (3e31775): comparison URL. Overall result: ❌ regressions - please read the text belowBenchmarking this pull request likely means that it is perf-sensitive, so we're automatically marking it as not fit for rolling up. While you can manually mark this PR as fit for rollup, we strongly recommend not doing so since this PR may lead to changes in compiler perf. Next Steps: If you can justify the regressions found in this try perf run, please indicate this with @bors rollup=never Instruction countThis is the most reliable metric that we have; it was used to determine the overall result at the top of this comment. However, even this metric can sometimes exhibit noise.
Max RSS (memory usage)Results (primary 1.1%, secondary 3.4%)This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.
CyclesResults (primary 1.3%)This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.
Binary sizeResults (primary 0.3%, secondary 0.3%)This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.
Bootstrap: 789.14s -> 789.465s (0.04%) |
@joboet The regressions are still somewhat significant, are you expecting to be able to claw back more performance with more manual implementations of This was discussed in the libs team meeting and we understand that this is obviously the right thing to do for correctness, but we're still concerned about the performance impact. |
I hope so. My gut feeling tells me that these regressions are all caused by just one or two cases where I need to readd the @rustbot author |
fixes #132442
std
specializes onCopy
to optimize certain library functions such asclone_from_slice
. This is unsound, however, as theCopy
implementation may not be always applicable because of lifetime bounds, which specialization does not take into account; the result being that values are copied even though they are notCopy
. For instance, this code:should not panic, but does (playground).
To solve this, this PR introduces a new
unsafe
trait:TrivialClone
. This trait may be implemented whenever theClone
implementation is equivalent to copying the value (so e.g.fn clone(&self) -> Self { *self }
). Because of lifetime erasure, there is no way for theClone
implementation to observe lifetime bounds, meaning that even if theTrivialClone
has stricter bounds than theClone
implementation, its invariant still holds. Therefore, it is sound to specialize onTrivialClone
.I've changed all
Copy
specializations in the standard library to specialize onTrivialClone
instead. Unfortunately, the unsound#[rustc_unsafe_specialization_marker]
attribute onCopy
cannot be removed in this PR ashashbrown
still depends on it. I'll make a PR updatinghashbrown
once this lands.With
Copy
no longer being considered for specialization, this change alone would result in the standard library optimizations not being applied for user types unaware ofTrivialClone
. To avoid this and restore the optimizations in most cases, I have changed the expansion of#[derive(Clone)]
: Currently, whenever bothClone
andCopy
are derived, theclone
method performs a copy of the value. With this PR, the derive macro also adds aTrivialClone
implementation to make this case observable using specialization. I anticipate that most users will use#[derive(Clone, Copy)]
whenever both are applicable, so most users will still profit from the library optimizations.Unfortunately, Hyrum's law applies to this PR: there are some popular crates which rely on the precise specialization behaviour of
core
to implement "specialization at home", e.g.libAFL
. I have no remorse for breaking such horrible code, but perhaps we should open other, better ways to satisfy their needs – for example by dropping the'static
bound onTypeId::of
...