-
Notifications
You must be signed in to change notification settings - Fork 58
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
What about: splitting struct into separate fields despite escaped references (maybe even raw pointers) #244
Comments
Why would |
That would use provenance to justify the optimization, which I think is very different from what @comex imagined. Also, this doesn't work when we talk about a raw pointer to the field instead of a reference. |
Sorry, to clarify: By "this" I meant I was talking about separating the fields of a struct even if there is an escaped pointer to a field – but not pointers to the struct itself, or to any other fields. So the field which had pointers to it would be laid out as normal, but the rest of the struct might not be. If At minimum, it should be fully semantics-preserving to allocate stack space for the entire struct, but only write to the field in question, leaving the rest as uninitialized memory. Really that's just a special case of aliasing assumptions combined with dead store elimination. As a stretch goal, it would also be nice to only allocate stack space for the field, but I suppose there are a few speed bumps. Even if the user wasn't allowed to dereference the result of
I don't know how feasible it is to actually enforce these restrictions, so in practice they might rule out the "only allocate space tor the field" optimization altogether. It might be interesting to consider whether the memory model could be adjusted to free the compiler from having to enforce them. But again, that's a stretch goal; wasting a bit of stack space is usually not a big deal. |
That is what I assumed. This relies on a view of memory not as a chunk of bytes, but as something "tree-like" that is structured according to the types of the data stored in there. C tries to do that. It is a mess.
The question is, how do you forbid them? Languages are not defined by saying with optimizations are allowed, they are defined by a concrete operational semantics that says what happens when you execute the program. To permit optimizations like the one you describe, that operational semantics has to somehow make it such that pointer arithmetic starting at one field of a struct cannot reach other fields of that struct. Provenance can do that, but only when aliasing assumptions come into play -- not when raw pointers are used. In that case the only avenue left is to make it so that the fields of a struct are not laid out next to each other in some byte-wise fashion, but instead form a tree in memory so that the fields are not next to each other linearly. But then you have to explain why
If the pointer-to-field is raw, there are no aliasing assumptions we can make. |
As I said, this optimization would not be applicable for raw pointers to fields. |
So you'd only do it if there were references to fields, but not if there were any raw pointers that escaped? Yeah that should work, if we adopt Stacked Borrows or something similar. That would not rely on a different view of memory, so it is a fundamentally very different proposal. It is in conflict with #134. |
Varargs potentially wants something like this: https://rust-lang.zulipchat.com/#narrow/stream/136281-t-opsem/topic/Varargs.20SROA.20sadness |
IIUC, Swift does not have field splitting / SROA optimization for unsafe pointers, but does for inout and shared arguments, because all values are semantically "in registers" and the only ways to semantically ensure a value actually has an address is the withUnsafe*Pointer(to:) family of intrinsic functions, or if the value is in a static variable. Reflecting back to Rust, this would result in new reference types |
With strict subobject provenance, I think the same would be allowed in Rust. I think you are basically suggesting we should have 2 reference types -- one that is strictly limited to the given type, and one that allows accessing surrounding memory under certain conditions? |
It sounds like the proposed non- |
What exactly does that even mean?
In fact we don't even need 2 types for this, just two different operations for taking a reference -- one that restricts provenance to the subobject, and one that does not. |
Something like: fn foo(x: &noaddr u8) {
assert!(x as usize == x as usize)
} is not guaranteed to succeed, because the semantics of the |
I would initially expect a specific reference to be consistent. But it not necessarily being so would also enable converting Plus the original spitball would prevent converting to |
How does that relate to SROA? Is the point that we must not be able to tell whether two references have the right distance to match the layout of the original struct? |
The point is that I agree re: the high bar of adding another pointer type, though. Having this kind of pervasive non-guaranteed address would be useful for a lot of things, but also problematic for many other things so I'm not sure we want to adopt this semantics for |
I'm not sure we need something so radical as |
I know that looks a bit odd when written that way, but it's similar to the non-guarantee around If we wanted to be more explicit about it, we would say that |
When |
At the latest. I think it could even be moved earlier, to when the struct is created. I don't think it makes sense to have new reference types just for this, either we find a way to retrofit SROA onto existing references, or we don't do it at all. But the set of guarantees I am picturing would look something like this:
|
@comex writes
My response is:
and then @comex asks
Not sure what you mean -- "this" = having byte-level untyped memory? This is what LLVM does, and to my knowledge all its optimizations are compatible with that model, so I'd say the impact is not big. But I am not an expert in what kinds of optimizations compilers (want to) do. I am coming more from the perspective of how to specify the language to enable optimizations that others tell me are desirable. Given that most languages have an ambiguous or no specification of what exactly constitutes UB, I feel like that's a niche I need to fill. ;)
Being able to separate the fields of a struct even when there are pointers to it is easy in languages with a high-level view of memory, but immensely difficult in C/C++/Rust which allow byte-wise memory manipulations. While it is possible to do so, I think the resulting semantics are inscrutable for programmers. So the real question here is, how much complexity in the spec (with the associated amount of bugs caused by misunderstandings, and complexity in Miri-like tools that aim to reflect the spec) are we willing to accept to enable such optimizations?
The text was updated successfully, but these errors were encountered: