-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
Default for arrays via const generics #74254
Conversation
r? @dtolnay (rust_highfive has picked a reviewer for you, use r? to override) |
Well, I think this PR works and can be reviewed. |
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 haven't reviewed closely yet, but the generated code looks quite a bit worse than before so I would be concerned about the performance and code size implications of this.
https://rust.godbolt.org/z/3dxGE5
extern crate core;
use core::mem::{self, MaybeUninit};
use core::ptr;
type T = String;
const N: usize = 2;
pub fn before() -> [T; N] {
Default::default()
}
pub fn after() -> [T; N] {
struct Wrapper {
data: MaybeUninit<[T; N]>,
init: usize,
}
impl Drop for Wrapper {
#[inline]
fn drop(&mut self) {
debug_assert!(self.init <= N);
let ptr = self.data.as_mut_ptr() as *mut T;
let initialized_part = ptr::slice_from_raw_parts_mut(ptr, self.init);
unsafe {
ptr::drop_in_place(initialized_part);
}
}
}
let mut w = Wrapper { data: MaybeUninit::uninit(), init: 0 };
let array_pointer = w.data.as_mut_ptr() as *mut T;
for i in 0..N {
assert!(N <= isize::MAX as usize);
unsafe {
let elem_ptr = array_pointer.add(i);
elem_ptr.write(T::default());
}
w.init += 1;
}
let data = unsafe { w.data.as_ptr().read() };
mem::forget(w);
data
}
example::before:
mov rax, rdi
mov qword ptr [rdi], 1
vxorps xmm0, xmm0, xmm0
vmovups xmmword ptr [rdi + 8], xmm0
mov qword ptr [rdi + 24], 1
vmovups xmmword ptr [rdi + 32], xmm0
ret
example::after:
sub rsp, 56
mov qword ptr [rsp], 1
vxorps xmm0, xmm0, xmm0
vmovups xmmword ptr [rsp + 8], xmm0
mov qword ptr [rsp + 24], 1
vmovups xmmword ptr [rsp + 32], xmm0
mov rax, rdi
mov qword ptr [rsp + 48], 2
mov rcx, qword ptr [rsp]
mov qword ptr [rdi], rcx
vmovups xmm0, xmmword ptr [rsp + 8]
vmovups xmmword ptr [rdi + 8], xmm0
mov rcx, qword ptr [rsp + 24]
mov qword ptr [rdi + 24], rcx
mov rcx, qword ptr [rsp + 16]
mov qword ptr [rdi + 16], rcx
mov rcx, qword ptr [rsp + 24]
mov qword ptr [rdi + 24], rcx
vmovups xmm0, xmmword ptr [rsp + 32]
vmovups xmmword ptr [rdi + 32], xmm0
add rsp, 56
ret
@bors try |
⌛ Trying commit 3d3a7bd02207ae5d2be3bd3ac776b4b0ea15e1e1 with merge 8205f8afb8c63490368efc0d9db81e4e2b5830d8... |
☀️ Try build successful - checks-actions, checks-azure |
The test failures seems like a problem with winnowing. The bound in
|
src/libcore/array/mod.rs
Outdated
// this is Default implementation for zero-sized arrays | ||
#[stable(since = "1.4.0", feature = "array_default")] | ||
impl<T> Default for [T; 0] { | ||
#[inline(always)] |
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.
The previous version did not have inline attr
Minimized: fn default_array<T, const N: usize>() -> [T; N]
where
[T; N]: Default
{
Default::default()
}
fn main() {
let _: [u8; 4] = default_array();
} |
src/libcore/array/mod.rs
Outdated
struct Guard<T, const N: usize> { | ||
data: *mut T, | ||
init: usize, | ||
} |
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 effectively implementing FromIterator
for arrays. I think extracting this making it reusable across other places would make sense.
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.
Well, I imagine something like:
// in core::mem
pub(crate) struct PartialBuffer<T, const N> {
buf: [MaybeUninit<T>; N],
init: usize
}
impl PartialBuffer<_, _> {
pub(crate) fn new() -> Self {..}
pub(crate) unsafe fn push(&mut self, value: T) {..}
// or maybe pub(crate) unsafe fn next_place(&mut self) -> &mut MaybeUninit<T> {..} to reduce copying
pub(crate) unsafe fn assume_init(self) -> [T; N] {..}
pub(crate) fn get_ref(&self) -> &[T] {..}
pub(crate) fn get_mut(&mut self) -> &mut [T] {..}
}
src/libcore/array/mod.rs
Outdated
#[inline] | ||
fn default() -> [T; N] { | ||
use crate::mem::MaybeUninit; | ||
let mut data: MaybeUninit<[T; N]> = MaybeUninit::uninit(); |
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.
Why not make the guard type itself hold the MaybeUninit?
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.
Previous version did so.
I tried keeping MaybeUninit outside of Guard to see whether it simplifies code and whether it optimizes generated ASM.
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.
Newest version of the code, in godbolt: https://rust.godbolt.org/z/Pc6175.
example::before:
mov rax, rdi
mov qword ptr [rdi], 1
vxorps xmm0, xmm0, xmm0
vmovups xmmword ptr [rdi + 8], xmm0
mov qword ptr [rdi + 24], 1
vmovups xmmword ptr [rdi + 32], xmm0
ret
example::after:
sub rsp, 48
mov rax, rdi
mov qword ptr [rsp], 1
vxorps xmm0, xmm0, xmm0
vmovups xmmword ptr [rsp + 8], xmm0
mov qword ptr [rsp + 24], 1
vmovups xmmword ptr [rsp + 32], xmm0
mov rcx, qword ptr [rsp]
mov qword ptr [rdi], rcx
vmovups xmm0, xmmword ptr [rsp + 8]
vmovups xmmword ptr [rdi + 8], xmm0
mov rcx, qword ptr [rsp + 24]
mov qword ptr [rdi + 24], rcx
mov rcx, qword ptr [rsp + 16]
mov qword ptr [rdi + 16], rcx
mov rcx, qword ptr [rsp + 24]
mov qword ptr [rdi + 24], rcx
vmovups xmm0, xmmword ptr [rsp + 32]
vmovups xmmword ptr [rdi + 32], xmm0
add rsp, 48
ret
It appears to me that the difference is entirely a consequence of "return value optimization" not kicking in for the MaybeUninit for some reason, which seems fixable in the compiler. Changing the following two lines fixes the performance issue and gets exactly the same machine code as before (but isn't correct).
- let mut data: MaybeUninit<[T; N]> = MaybeUninit::uninit();
+ let mut data: [T; N] = unsafe { mem::uninitialized() }; // unsound
- unsafe { data.assume_init() }
+ data
example::before:
mov rax, rdi
mov qword ptr [rdi], 1
vxorps xmm0, xmm0, xmm0
vmovups xmmword ptr [rdi + 8], xmm0
mov qword ptr [rdi + 24], 1
vmovups xmmword ptr [rdi + 32], xmm0
ret
example::after:
mov rax, rdi
mov qword ptr [rdi], 1
vxorps xmm0, xmm0, xmm0
vmovups xmmword ptr [rdi + 8], xmm0
mov qword ptr [rdi + 24], 1
vmovups xmmword ptr [rdi + 32], xmm0
ret
@MikailBag thanks for this PR, we ended up talking about this implementation and the winnowing problem on zulip: https://rust-lang.zulipchat.com/#narrow/stream/122651-general/topic/good.20example.20for.20const.20generics/near/203642562 Do you want to try and incorporate that impl in your PR or should I open a new one for it? /// A trait implemented by all arrays which are either empty or contain a type implementing `Default`.
#[unstable(feature = "array_default_internals", reason = "implementation detail", issue = "none")]
#[marker]
pub trait ArrayDefault {}
#[unstable(feature = "array_default_internals", reason = "implementation detail", issue = "none")]
impl<T> ArrayDefault for [T; 0] {}
#[unstable(feature = "array_default_internals", reason = "implementation detail", issue = "none")]
impl<T: Default, const N: usize> ArrayDefault for [T; N] {}
trait DefaultHack {
fn default_hack() -> Self;
}
impl<T> DefaultHack for T {
default fn default_hack() -> Self {
unreachable!();
}
}
impl<T: Default> DefaultHack for T {
fn default_hack() -> Self {
Default::default()
}
}
#[stable(since = "1.4.0", feature = "array_default")]
impl<T, const N: usize> Default for [T; N]
where
[T; N]: ArrayDefault
{
// ...
} |
@dtolnay thanks for your wonderful investigation!
|
3 seems best if possible. |
If we are going to add back the |
@dtolnay I implemented third approach, tests pass (wow 😄 ). |
I do not feel comfortable with the current approach.
#![feature(const_generics)]
fn break_me<T, const N: usize>() -> [T; N]
where
[T; N]: Default
{
Default::default()
}
fn main() {
let _: [u8; 35] = break_me();
} I personally would like to try and perf what happens if we just use const generics for all impls and
While these impls are bound on an unstable trait, they are still instantly stable. |
The current impl also fails for #![feature(const_generics)]
struct Foo<const N: usize>([u8; N]);
impl<const N: usize> Foo<N> {
fn new() -> Self {
Foo(Default::default())
}
}
fn main() {} with
|
@lcnr thanks, it's really serious argument against this combined "macro + const-generics" approach, especially the last example. Let me summarize how I see situation. |
I feel like this is the best final solution, and it would probably benefit multiple other cases. |
Unfortunately, PR does not play well with
I currently changed the feature from |
Please do not. |
Yeah, I understand that using non-min specialization is only workaround. I have zero experience with trait system, so I can be wrong here. // We allow specializing on explicitly marked traits with no associated
// items. And Default does not have associated types. That's why I now think I should add this specialization marker to Default and UPD: no, build fails:
|
It is backwards-incompatible to add this marker to an existing trait. And it is unsound to do specialization on a trait unless the trait has that marker -- a trait that is used for specializing other traits needs to pass some stricter checks, which most existing traits will not pass. I don't know of a good way out of this, but I am also not a specialization expert. Cc @matthewjasper |
This is backwards incompatible and looks highly likely won't be merged as is. If you find a different way to solve this, it is better to create a new PR. Thanks for taking the time to contribute |
@Dylan-DPC can you please describe in what way this PR breaks compatibility? I don't know if the desired behavior can be achieved with |
Can't we just introduce a new #[rustc_unsafe_specialization_marker]
trait MarkerDefault: Default {}
impl<T: Default> MarkerDefault for T {}
trait DefaultHack {
fn default_hack() -> Self;
}
impl<T> DefaultHack for T {
default fn default_hack() -> T {
unreachable!();
}
}
impl<T: MarkerDefault> DefaultHack for T {
fn default_hack() -> T {
Default::default()
}
} |
We should IMO not introduce more |
There's a specialization-free approach, waiting for the feature |
Sorry, I created PR too early :)
It is WIP currently.
UPD: no longer WIP, but blocked on codegen issues.