-
Notifications
You must be signed in to change notification settings - Fork 308
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
RFC: Array Reference Type #879
Comments
Cool, I think it's a good idea. The core of this idea has been attempted - I'm 90% sure we've talked about it but I can't see it in the issues, and I've played around with this as a code change. But never so fleshed out as this (To be clear - now it's a much better time to do it too, it feels like we know what we are allowed to do in unsafe Rust to a bigger extent now). I would be tempted to start with having I would like to reduce the monomorphization bloat. Do you have any example in particular where you have experienced this? And yes, traitification has been the solution I have favoured but it seems like it would be good to do both. I agree with everything you've written. I believe the With this - Borrow and all - can we then deprecate |
Now that you mention it, I vaguely recall this too.
I thought about this, but it would require
A fairly minimal starting point would be to implement all of the RFC except for the "moving most methods to be implemented only on
It's not something I've investigated or noticed in practice (other than maybe compile times?); it's just something I'm aware of. Basically, by changing function parameter types to be
Yeah, long-term, I think a trait-based approach is the way to go.
I assume you mean
This RFC would make it possible to use |
Oh thanks for spelling that out, I guess once I was "set" on the separate It seems like as defined And yes, I have been trying to reduce impl bloat but this sounds like a good idea anyway! (like #819 etc) |
Oh, good point. I didn't think of that. I tried a simplified example, and you're right:
A workaround is to add a |
This might solve the long standing += operator papercut. The thing that says that we can't do the operation The separate structs are needed sooner or later I think, otherwise the (I'm just adding pros and cons as I find them) |
This does, indeed, appear to be the case. This is a simplified, quick-and-dirty example in the playground.
I could go either way on this. I've added my thoughts on this to RFC above. More details about the "easing the transition":
|
Do we have a reference for the fact that casting between pointers to these two structs (*const ArrayBase to *const ArrayRef) that have identical common prefix, is legal? |
I've updated As far as I can tell, the cast is legal. The Rust reference documents the C representation in detail.
That said, I may be missing something. The guaranteed-safe option would be to make struct ArrayMeta<A, D> {
dim: D,
strides: D,
ptr: NonNull<A>,
}
pub struct ArrayBase<S, D>
where
S: RawData,
{
meta: ArrayMeta<S::Elem, D>,
data: S,
}
mod array_ref {
#[repr(transparent)]
pub struct ArrayRef<A, D> {
meta: ArrayMeta<A, D>,
}
impl<A, D> ArrayRef<A, D> {
pub(crate) fn meta(&self) -> &ArrayMeta<A, D> {
&self.meta
}
}
impl<A, S, D> Deref for ArrayBase<S, D>
where
S: Data<Elem = A>,
{
type Target = ArrayRef<A, D>;
fn deref(&self) -> &ArrayRef<A, D> {
let meta_ptr = &self.meta as *const ArrayMeta<A, D>;
unsafe {
&*meta_ptr.cast::<ArrayRef<A, D>>()
}
}
}
impl<A, S, D> DerefMut for ArrayBase<S, D>
where
S: DataMut<Elem = A>,
{
fn deref_mut(&mut self) -> &mut ArrayRef<A, D> {
self.ensure_unique();
let meta_ptr = &mut self.meta as *mut ArrayMeta<A, D>;
unsafe {
&mut *meta_ptr.cast::<ArrayRef<A, D>>()
}
}
}
}
pub use array_ref::ArrayRef; The reason for keeping |
About the reference to docs - that's neat. I was hoping we'd find it spelled out when exactly it's legal to cast between pointers to two different struct types. In C we can't do this pointer cast even if the types are in theory representation identical - but that's due to type-based alias analysis which we don't have in Rust. The following RFC seems to be a very current strain of the representation compatiblity discussion rust-lang/rfcs#2981 About separate struct or not - there seems to be downsides with either alternative. if |
Okay, I understand. I didn't know that about C; that sounds really limiting. I thought manipulations like this were somewhat common in C. Do people work around the alias analysis by converting pointers to integers and back again (to break pointer provenance), use unions, or something else? I think the cast to As far as implementing the RFC goes, I'd be happy for someone else to implement it. I wrote up this RFC now because I think that it's a good idea that'll solve problems with the current API, and I didn't want to forget about it. If no one else implements it, I'll probably get around to it eventually, but it'll be a while. (I'd guess at least half a year.) |
C is tricky. The most useful rule is that one can cast a struct pointer to and from the first member's pointer https://stackoverflow.com/questions/9747010/does-accessing-the-first-field-of-a-struct-via-a-c-cast-violate-strict-aliasing And then type punning through a union is ambiguous if it's really a rule, https://stackoverflow.com/questions/11639947/is-type-punning-through-a-union-unspecified-in-c99-and-has-it-become-specified With that said, I think the casts in this RFC feel right but it makes sense to make sure every step in the reasoning here has support. |
I would be remiss not to mention the compiler option (in C) "-fno-strict-aliasing" which turns off the strict type-based aliasing rules. Many projects use that so that they can have more fun. That's pragmatic but it's basically turning off one of the language rules. |
Hi! I hope this isn't presumptuous, but I've started an implementation of this RFC; I figure if nothing else, it will let me really understand I've opted for the "separate struct" implementation as prototyped by @jturner314's comment above, and just wanted to mention it and address one note that I've found so far. I think that the various I'm also wondering whether If you're curious, the implementation (which doesn't even build yet) is located here. |
Ok, I've made some progress on the implementation and (in doing so) gotten a much stronger understanding of First off, if I've misunderstood an issue, please stop here and let me know. I've been thinking this over and I would like to discuss a few solutions. The first is to essentially re-implement all methods that are appropriate for raw arrays, but I strongly dislike that much copied code. It could potentially be done by using a macro that implements a given behavior for both, but this feels a little like using macros to paste over poor design. Either way, implementing functions for both The second idea was to move closer to a design suggested at the end of the RFC: rely on traits a bit more, but also push the idea of
Furthermore, we'd like to push as much code as possible down to the
The problem is, I really cannot find a way to implement So I circle back and am left with using either a macro (if we want to hide Any ideas are greatly welcome; pointing out that I missed something and this really is possible would be even more welcome 😁 thanks! |
@akern40, it's great that you started implementing this RFC! Here are some comments. (I'm new to ndarray internals, and still pretty new to Rust, so I might be writing nonsense.)
|
Hey @grothesque! Glad you're interested, thanks for the comments. I saw you also commented on the maintainer issue - I'm just gonna keep this thread about this particular RFC and I'll get back to you in the other issue on the broader topic. Design InsightsI've made some more design progress and, along the way, identified a few critical design decisions and implications. I'm going to try to outline them here as briefly as I can. Please keep in mind that all of these insights are only applicable insofar as I can figure out a design for them. Someone may come along on any one of these and provide a better path forward.
Design ProposalSo to really crystallize my thoughts and progress, I've created what I'm hoping is the first of many gists. This one shows how to design a trait- and deref- based API without the ability to wrap the underlying pointer in There's a lot I'd like to say about this design, but this is already a long post so here's the summary:
This design achieves the following desired properties:
Anyway, please let me know what you think! As I said above, I'll get to your other comment on the other issue soon. |
Please post your thoughts, or let me know if something is unclear/confusing!
Summary
This RFC proposes the addition of a new storage type
RefRepr
and associated type aliasesArrayRef<A, D>
,ArrayRef1<A>
,ArrayRef2<A>
, etc., such that we can implement the following traits:Borrow<ArrayRef<A, D>>
forArrayBase<S, D> where S: Data
Deref<Target = ArrayRef<A, D>>
forArrayBase<S, D> where S: Data
BorrowMut<ArrayRef<A, D>>
forArrayBase<S, D> where S: DataMut
DerefMut<Target = ArrayRef<A, D>>
forArrayBase<S, D> where S: DataMut
ToOwned<Owned = Array<A, D>>
forArrayRef<A, D>
&'a ArrayRef<A, D>
would be analogous toArrayView<'a, A, D>
, and&'a mut ArrayRef<A, D>
would be analogous toArrayViewMut<'a, A, D>
.Many of the methods on
ArrayBase
would be moved to be implemented only onArrayRef
instead, and be available through deref coercion, similar to how much of the functionality ofVec<T>
is actually implemented on[T]
.Motivation
Good way to express function parameters
Consider a function that needs a view of a 2-D array of
f64
elements. With currentndarray
, we have the following ways to express this:Each of these has disadvantages:
Options 1 and 2 are verbose, and become particularly bad for functions taking many array parameters.
Option 4 is unnatural for callers of the function. (In most cases, they have to call
.view()
on the argument, which is verbose and is different from most Rust code, which typically uses references for this use-case.)Options 1, 2, and 3 are generic, so they suffer from monomorphization bloat, as discussed in the next section. For functions taking multiple array parameters, it's even worse—the number of possible type combinations is exponential in the number of array parameters. (To minimize the bloat, it's necessary to create an inner function like option 4, and make the outer function convert the parameter(s) to
ArrayView
and call the inner function.)With this RFC, it will be possible to express the function like this:
This is more concise than the other options, and it's not generic, so there is no monomorphization bloat.
Mutable views would be handled similarly, just with
&mut
instead of&
.Substantially reduce monomorphization bloat
Most of the functionality of
ndarray
is currently implemented as methods onArrayBase<S, D>
. As a result, the methods are monomorphized for each new combination ofS
andD
types, which means that there's a lot of nearly-identical generated code, i.e. "monomorphization bloat". The same issue occurs for crates which provide extension traits, such asndarray-linalg
,ndarray-stats
, andndarray-npy
. (It's possible to minimize monomorphization bloat for a method by changing it to convert everything toArrayView/Mut
and then call an inner function which takesArrayView/Mut
parameters. However, that's very verbose and tedious.)If, instead, we move most functionality to be implemented only on
ArrayRef<A, D>
, analogous to how much of the functionality ofVec<T>
is actually implemented on[T]
, then the monomorphization bloat due to differing storage types would be eliminated. (Callers would rely on deref coercion, like they do for methods actually implemented on[T]
forVec<T>
.)Enable in-place arithmetic operator syntax for the result of a slicing operation
Currently, it's not possible to use in-place arithmetic operator syntax for the results of slicing operations; instead, it's necessary to call the appropriate method:
The error message is
By making
ArrayBase
dereference toArrayRef
and implementing the in-place arithmetic operators onArrayRef
, this will compile without errors.Better integration into the Rust ecosystem
The existing Rust ecosystem provides functionality dependent on traits like
Borrow
. Implementing these traits will allowndarray
to interact more cleanly with the rest of the ecosystem. See #878 for one example.Guide-level explanation
In many ways, an owned array
Array<A, D>
is similar toVec<A>
. It is an allocated container for a collection of elements.The new
ArrayRef<A, D>
type for arrays is analogous to the[A]
(i.e. slice) type for vectors. Arrays dereference toArrayRef<A, D>
, likeVec<A>
derefs to[A]
, so functionality available forArrayRef<A, D>
is available for other array types through deref coercion, like how functionality available for[A]
is available forVec<A>
through deref coercion.When writing a function, you should use
&ArrayRef<A, D>
or&mut ArrayRef<A, D>
as the parameter types when possible, similar to how you'd use&[A]
or&mut [A]
instead of&Vec<A>
or&mut Vec<A>
.In general, handle
ArrayRef<A, D>
analogously to how you'd handle[A]
. The primary ways in which they are not analogous are the following:ArrayRef<A, D>
isSized
, unlike[A]
. As a result,&ArrayRef<A, D>
is a thin pointer, while&[A]
is a thick pointer.Due to technical limitations, given an array it's not possible to directly create an
&ArrayRef
that is a view of a portion of the array. In other words, you can create a slice (i.e.&[A]
) of a portion of a&[A]
/Vec<A>
with indexing, e.g.&var[3..12]
, but this is not possible for&ArrayRef
. In order to create an&ArrayRef
that references a portion of an array, you have to first create anArrayView
(by slicing, etc.), and then coerce thatArrayView
toArrayRef
.Also note that
&'a ArrayRef<A, D>
is similar toArrayView<'a, A, D>
, and&'a mut ArrayRef<A, D>
is similar toArrayViewMut<'a, A, D>
, with the following exceptions:You don't have call a method like
.reborrow()
(forArrayView/Mut
) to manually shorten the lifetime of the borrow. The lifetimes of references toArrayRef
will be automatically adjusted as necessary, just like other references in Rust.The shape, strides, and pointer of an
&ArrayRef<A, D>
or&mut ArrayRef<A, D>
cannot be modified. For example, you can't call.slice_axis_inplace()
. To get an array with different shape/strides/pointer, you have to create a view. For example, you could call.slice_axis()
, or you could create a view with.view()
and then call.slice_axis_inplace()
on the view.Implementation
Implementing
Deref
andDerefMut
withTarget = ArrayRef<A, D>
The new
RefRepr
storage type will be:The
ArrayBase
struct will be changed to the following:The important parts of this change are:
The
dim
,strides
, andptr
fields are the first fields in the struct.The struct is declared
repr(C)
.These changes are necessary so that it's possible to implement
Deref
andDerefMut
by casting references toArrayBase
into references toArrayRef
.@bluss pointed out below that it's necessary to avoid infinite recursion in deref coercion. To do this, a
DataDeref
trait will be added:This trait will be implemented by all storage types except for
RefRepr
and will be used as a bound for theDeref
andDerefMut
implementations.Deref
andDerefMut
will be implemented like this:Moving most existing methods to be implemented only for
ArrayRef<A, D>
Most of the existing methods which don't modify the shape/strides/pointer (such as
slice
,mapv
, etc.) will no longer be implemented for allArrayBase<S, D>
, but rather will be implemented only onArrayRef<A, D>
. This change isn't necessary in order to introduce theArrayRef
type, but it is necessary in order to reduce the existing monomorphization bloat, which is one of the primary benefits of theArrayRef
type.Data traits for
RefRepr
The
RefRepr
type will implement the following traits:RawData
RawDataMut
Data
(but make theinto_owned
implementation panic withunreachable!()
)DataMut
RawDataSubs
In other words, the
&'a
or&'a mut
in a reference to anArrayRef
will control the lifetime and mutability of the data.We need to audit all of the available methods to make sure that:
&'a ArrayRef<A, D>
only for a base array withS: Data
that lives at least as long as'a
,&'a mut ArrayRef<A, D>
only for a base array withS: DataMut
that lives at least as long as'a
, andArrayRef<A, D>
(I believe this is already true if we just add the
Deref
,DerefMut
,Borrow
, andBorrowMut
implementations, but we should check.)Preventing modification of shape/strides/pointer via
&mut ArrayRef<A, D>
To minimize confusion and behave analogously to
&mut [A]
, it should not be possible to modify the underlying shape/strides/pointer through a&mut ArrayRef<A, D>
. For example,To achieve this:
A new
MetaMut
trait will be added to control whether the shape/strides/pointer can be modified.MetaMut
will be implemented for all of the existing storage types (OwnedRepr
,ViewRepr
, etc.), but not the newRefRepr
type.A
S: MetaMut
bound will be added to all methods which modify the shape/strides/pointer in-place (such asslice_collapse
,slice_axis_inplace
,collapse_axis
,swap_axes
,invert_axis
,merge_axes
,insert_axis_inplace
, andindex_axis_inplace
) .Drawbacks
This proposal introduces yet another storage type, when
ndarray
already has quite a few.The similarity of
&'a ArrayRef<A, D>
toArrayView<'a, A, D>
and&'a mut ArrayRef<A, D>
toArrayViewMut<'a, A, D>
could be confusing to new users.The requirement that it must be possible to create
ArrayRef
references by reinterpreting the bits in existingArrayBase
instances limits the possible field layouts ofArrayBase
.Rationale and alternatives
Allow mutation of the shape/strides/pointer for
&mut ArrayRef<A, D>
It would be possible to avoid the new
MetaMut
trait by allowing modification of the shape/strides/pointer through&mut ArrayRef<A, D>
instances. However, this would likely be confusing to users who are familiar with how&mut [T]
/Vec<T>
behave, and it would increase the potential for bugs.Keep most method implementations on all
ArrayBase<S, D>
It would be possible to keep the existing method implementations for all
ArrayBase<S, D>
instead of moving them to be implemented only onArrayRef<A, D>
. However, this would not bring the reduced monomorphization bloat which is one of the advantages of the newArrayRef
type.Make
ArrayRef
be a separate struct fromArrayBase
It would be possible for
ArrayRef
to be a separate struct fromArrayBase
, instead of an alias ofArrayBase
. This would have the following advantages:Deref
,DerefMut
,Borrow
, andBorrowMut
could be implemented forArrayBase
withoutunsafe
code.MetaMut
andDataDeref
traits wouldn't be necessary.The disadvantages of a separate struct type are:
The transition to
ArrayRef
would be more disruptive. Until libraries update their functions to take&ArrayRef
or&mut ArrayRef
parameters instead of&ArrayBase
or&mut ArrayBase
parameters,ArrayRef
would not be compatible with those functions. (Users of those libraries would likely have to call.view()
/.view_mut()
in some places.)It may be less convenient for generic code which wants to operate on all array types.
For example, if
ArrayRef
is not a separate struct, then these impls are sufficient forAddAssign
:(Note that, unfortunately, we can't replace
impl<'b, A, B, S2, D1, D2> AddAssign<&'b ArrayBase<S2, D2>> for ArrayRef<A, D1>
withimpl<'b, A, B, D1, D2> AddAssign<&'b ArrayRef<B, D2>> for ArrayRef<A, D1>
. For some reason, the compiler has trouble figuring out which impl to use whenimpl<A, B, D> AddAssign<B> for ArrayRef<A, D>
also exists.)However, if
ArrayRef
is a separate struct, then it would be necessary to add another impl:Custom DST (i.e. custom thick pointer types)
Instead of using thin pointers to
ArrayRef
, we could wait for custom DSTs (e.g. RFC 2594) to be added to Rust, and then create a custom DST similar toArrayRef
. This would have the following advantages:Since the shape/strides/pointer would be stored within the thick pointer, it would be possible to modify the shape/strides/pointer in the thick pointer in-place, unlike for a reference to an
ArrayRef
.Slicing methods could return a thick pointer instead of an
ArrayView/Mut
except in theIxDyn
case.Raw thick pointers (i.e.
*const
/*mut
) would be more useful. In other words, since the shape/strides would be stored within the thick pointer itself, it should be possible to access that information without dereferencing the pointer. So, except in theIxDyn
case, thick pointers could replaceRawArrayView/Mut
. In contrast, given a*const ArrayRef<A, D>
or*mut ArrayRef<A, D>
, you have to dereference the pointer to access the shape/strides, soArrayRef
cannot replace most uses ofRawArrayView/Mut
.However, I'm not aware of any Rust RFC for custom DSTs which would make it possible to create a custom DST with support for the dynamic-dimensional (
IxDyn
) case. So, there would be this weird dichotomy between the convenience of thick pointers for the const-dimensional case and the complexity of the dynamic-dimensional case. In contrast, theArrayRef
type proposed in this RFC would support the const-dimensional and dynamic-dimensional cases in a uniform way.Future possibilities
One thing I wanted to make sure when writing this proposal was that it would still allow replacing the
ArrayBase
struct with a trait-based API in the future. In other words, if we hadinstead of having all arrays be instances of
ArrayBase
, it would be nice for this proposal to still apply. This proposal would, in fact, be compatible with a trait-based API, and the implementation could be somewhat cleaner because it wouldn't require anyunsafe
code. I'd suggest implementing the types like this:The text was updated successfully, but these errors were encountered: