-
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
Representation of structs #11
Comments
I for one would like someone to talk a bit about what Is it worth maybe trying to tease those out into distinct repr flags (any lang changes would require an RFC, of course). There may also be lesser variants that are useful: for example, I think it would be useful to be able to say that "the first field appears at offset 0" but say nothing about the positions or layouts of other fields within a struct. It'd be good -- while we're thinking about this stuff -- to think about the kinds of things we might want to say but currently cannot. |
The C11 spec provides some fairly detailed guidance on structure layout, which we could apply to |
The C17 standard is here: http://www.open-std.org/jtc1/sc22/wg14/www/abq/c17_updated_proposed_fdis.pdf The
I think that we should just say that |
Let's say the compiler keeps the freedom to choose arbitrary layouts at compile time, would punning be valid after checking that the layout just happens to be identical (via some E.g. as an optimization to just cast between |
On Fri, Aug 31, 2018 at 12:14:02PM +0000, gnzlbg wrote:
Ignoring bitfields and other things that do not apply to Rust,
...yet. :)
|
I'm going to stake out a tentative position:
|
My quick two cents on easy questions before I tackle the big issue of what to do about repr(Rust):
Yes, this should definitely be valid if done properly. The checks can get quite involved though, if we truly guarantee nothing about repr(Rust) layout, one would have to check at minimum:
repr(C) also prescribes the amount and locations of padding, but I don't think this makes repr(in_order) useful: we can't lower the alignment requirements of the fields, and raising it (edit: or alignment of the struct itself) is basically useless. It can theoretically facilitate the use of instructions with higher alignment requirements (e.g., SIMD) or prevent false sharing, but both of those are very tricky and niche even when done by a programmer, and automatically detecting when these opportunities are present and justifying them against the increased padding (and the cache misses, memory bandwidth, peak rss, etc. implied by it) seems infeasible to me. |
I personally feel that we shouldn't add I also hope we don't need Personally, I would greatly prefer that we handle structure ordering issues by helping developers reorder the actual structure order in the source, rather than deciding to reorder it at compile time. If you write a struct with a |
Just to make sure I understand you: you would prefer that we respect the field ordering that the user gave? Or you hope that users never care about prefix?
Interesting. I feel the opposite. If the developer wants field order to be preserved, I would rather they say so, as I think it's an exceptional case and I'd like my attention to be drawn to it. Otherwise, I would prefer that I have the freedom to order my fields in a way that maximizes readability of my source, and without regard for "bitpacking" or other things. |
I think we already talked about this on IRC, but for the record, I do find these ideas for new Once we have the guidelines closer to a finished state, if someone wants to fill an issue in the rfc repo to discuss these, or fill in a rfc, then we can discuss these and how to incorporate them into the guidelines there. Maybe the issues could be filled there already so that we don't forget? |
Preserving order by default does make even less sense when generic fields are present. |
I do agree that we shouldn't invent new features as part of the unsafe code guidelines, though we certainly might use the process to inform the design of new language features. @nikomatsakis I would hope that users could have multiple structs with a common prefix and read that common prefix without needing a special directive to do so. Regarding the second point: structure order can matter sometimes, and I'd prefer that code be self-documenting about that. It's one thing if you don't already lay out your structure for optimal packing. But if you do, it'd be surprising to have the compiler reorder for other reasons, such as expected locality optimizations or profiling. I'd much rather have that information used to guide the developer into making such changes. |
I absolutely agree that
It's not surprising to me, as a developer who rarely in practice has to concern myself with low-level bit manipulation, that no guarantees are made other than those explicitly promised. If I am writing code that cares about common prefixes or field order, I'm already off the beaten path. And I'm in unsafe code, which means that I have to be watching myself carefully. I know that there are many pitfalls and caveats, and that I can't just make assumptions.
Because the vast majority of users do not want to ever have to think about this stuff. They want the compiler to just do the optimization. There will be questions in #rust-beginners to the effect of "why doesn't the compiler just do it itself?" and "because some unsafe code people prefer it this way" is very far from a satisfactory answer. It will become a serious turn-off for new users, particularly when they add a new field to an existing struct and get ceremoniously informed that they have done so in the wrong place. It will hold up code reviews as people have to go "Sorry, the build failed because we have If users care about field order in source code, they want it to be in some logical order. Maybe it's alphabetical, maybe it's grouped by purpose. Whatever it is, it definitely not "the order that leads to the least packing". |
@nikomatsakis wrote:
There are some cases where one can legitimately know and exploit that two distinct repr(Rust) structs have identical contents, without breaking privacy/abstraction barriers and without being otherwise "sketchy". Most commonly, there are "plain old data" types with all fields public and a Another example might be writing a compatibility shim between two different crates (or major versions of the same crate) solving a common problem with different APIs. If the author of this shim is also the author of the other crates involved, they can know that some relevant data structure is defined the same in both crates and will remain so in future releases, even if it's not public. Given the knowledge that two types are layout compatible, what can we use it for? For one, we can receive a large collection containing items of one type, and pass it to other code expecting a collection of the other type without copying. We can also implement I'm sure there's more examples, but I hope this establishes type punning of other crates' types is not limited to sketchy rule-breaking. To top it off, here's a concrete example of something I might want to do: Suppose I load a 3D model from disk using the |
Fair enough! I think an example of where PGO might be useful that would break some of these invariants is adding padding to reduce false sharing for things that cross threads. But perhaps this ought to be handled by informing the user where to add annotations, instead, as @joshtriplett suggested. I find that appealing, particularly since it means that one could compile again (without profile data) and still see the same optimizations. So this is a concrete thing we could decide here:
In particular, are we willing to say that the layout of a struct is a function of the types of its fields? Or do we want the freedom to examine how the struct is used? Does anybody even do PGO of the kind I am talking about? (I have no real idea) |
I don't thikn we can "specify features that Rust doesn't have yet", but I definitely think we should take note of gaps that exist and file them for future follow-up. |
I think I feel quite differently here. Even when I was a "young C hacker", I always found myself writing comments when I was writing a struct where field ordering was important -- it seems like a subtle thing that is worth documenting! (And I second @alercah's arguments as well.) I think this is the position I currently favor:
But I'm curious to hear from people who have experience using PGO-like optimizations in the wild. I'd also be curious to hear what @eddyb thinks on the particular point of whether layout should be deterministic. |
But if you wanted to rely on such things then the order of fields would suddenly become part of the API contract of crates, i.e. someone just reordering their fields for code formatting reasons would suddenly become an API break if punnability were guaranteed for At a minimum you'd still need some compile-time verification that they are layout-compatible. |
This seems like a bare minimum, yes. I'd also love to provide enough information that people can know when their structs get reordered, and at the very least some way to explicitly enable a lint to get the compiler to say when it reorders so that the developer can incorporate that.
This is precisely what I meant when I said that the compiler should tell you rather than just doing it. |
PGO: Related to false sharing, there are other classes of optimizations that the compiler could make by rearranging fields. For instance, suppose we have a struct containing 3 halfwords, one which is written to much more frequently then the others (note that Even without resorting to strange architectural properties, it is probably faster to load two subword variables in a single load and then mask them out. So the compiler may want to move fields frequently accessed together into offsets where it can minimize the total number of loads and stores. The possibility that this is architecture-dependent makes me think that any manual input to this process may be better off in the form of annotations on fields, rather than asking for specific layouts, as the optimal layout may be architecture-dependent. It's possible that our technology today is not really at the point where we can take advantage of this. But we're also in a world where machine-learning-optimized compiles may be a thing in the near future, so I'm inclined to say that we should not close the door to these sorts of improvements. Punning: It seems to me that we can separate punning into three cases: a) where the user owns both types b) where the user owns only one type and c) where the user owns neither type. Case a) doesn't worry me too much. Especially if, as I suggested in the tuples thread, we allow the user to explicitly specify that the layout of a struct matches the tuple with the same fields, then it's easy enough for the user to so annotate both of the types involved (and there may be benefit to letting them do so in a way other than As for b) and c), I realized something just today: if we say that layout is deterministic, then field order becomes part of a type's API. If we provide a guarantee that you can take some This implies to me that scenario c) is something we can never guarantee in general, unless we add some additional information than field type to the process (e.g. lay out all fields of the same size in lexicographic order by name). Although we could in theory do so for tuple-like structs, I think that most of the things people are interested in punning have nominal fields anyway. This made me wonder: why are we asking users to do type punning themselves, anyway? There's two reasons: either they really care about performance, or they just don't want to write out the fields. We can solve the latter through better language features (or even just better optimizations: if the compiler was capable of recognizing when a move/copy was being done fieldwise between two identically-laid out types, it could just optimize to a single wide move/copy and this would work for data that was owned, although not for punning a reference or pointer to unowned data), and those features would benefit all conversions between similar types. For the former, we could either flat-out say that we do not support this, or we could offer safe APIs that allow for punning. For instance, we could have one For b), we're in a boat where we can't safely make any such guarantee today, but we could in theory add a way to say "my type is laid out the same as this other one", which would be robust against changes to the remote type's layout. Furthermore, any language features or optimizations like the above would apply here. |
Apropos of none of the above things: I think we can probably safely guarantee that a DST appears last in memory. |
Considering that PGO is frequently mentioned and struct-layout-randomization could conceivably also be used as some form of testing that does not seem all that self-evident. |
In #12 (comment) @ChrisJefferson linked these two papers:
Neither is directly applicable, though. The first is about Java where allocations tend to have different shapes than in Rust (and reports mixed results, indicating to me that this should be opt-in), the latter does much more aggressive transformations that we can't allow in Rust except under the as-if rule. |
Good point, though this still leaves tuple structs where order is already significant.
One should have that in any case so that a semver break (uncool but happens) does not turn into a memory safety issue (very bad). |
As has been pointed out before, for generic types the optimal order may be different depending on type parameters, making it impossible to even write down the optimal order in the source code. I'll add that the optimal order can also vary by target platform, e.g. due to:
Additionally, for the metric "minimize overall structure size" or "minimize padding while aligning this field to K", it is quite feasible to find optimal solutions quickly, reliably and automatically. It's silly to force programmers to think about and put into their source code an optimization that the compiler can handle 100% In contrast, for metrics such as "reduce false sharing", "reduce cache misses", etc. an "optimal" solution is very hard to find for many reasons (e.g. it depends on runtime memory layout and uarch details, and even ignoring those the idealized mathematical optimization problem is computationally hard). Add in that a PGO build is more expensive than a normal optimized build and it's quite obvious to me why a programmer might want to invest time once to learn about layout changes that seem profitable, investigate them, and then put them in writing if they seem good. |
Just wanted to say, with regards the papers I mentioned in earlier, I agree these papers don't directly apply to Rust, but they show that people do do research into layout in various ways -- it's not surprising there isn't (yet) research into layout in Rust, the language isn't old enough yet! Anything we fix today will be, in practice, fixed forever (at least experience with past languages suggest this will be the case), so I'd very much prefer we not fix anything, just because people think varying it won't be useful for optimisation. At the moment we have On the other hand, I know academics who haven't bothered doing research into layout of C and C++, because as the languages define the layout, any system which automatically changed layouts for all structs in a program would have a good chance of breaking things, and trying to detect which structs/classes can have their layout changed is a non-trivial activity. I personally don't want to investigate which layout changes might be profitable, in the same way i'm not interested in which functions are worth inling. I'd prefer a PGO to just "go forth" and do it's best attempt at speeding up my program. |
Sure, but that doesn't matter across crate versions. |
It seems to me that changing the order of (public) fields in a tuple struct is already a breaking change (particularly if those fields have the same type, which I believe is the only case where we said that we might want field order to matter). In particular, if the fields are public, your clients can write: let Foo(name, age) = bar(); and now they have extracted out a The only way that it wouldn't be a breaking change would be if the order doesn't matter, in which case.. it doesn't matter. Right? (I think this is maybe what @alercah was getting at? Not sure.) EDIT: Re-reading, I think this was exactly @rkruppe's point and I was just confusing myself... |
Yes, that was exactly my point, but since two people have replied restating this at me, I clearly must have worded it very badly 🤔 |
I'm not clear why changing the order in memory of these things would effect the behaviour of tupper structs. As long as rust maps the first element of the struct to (in this case) name, it doesn't matter how the valuee are padded, or their order? |
I opened #31 which contains my attempt to summarize our tentative consensus here. Please give feedback! (In particular, let me know if there is anything you think is incorrect.) |
Are there any guarantees about how changing a type parameter affect the memory layout of a struct when it is only ever used in a Example:
I am currently relying on the layout of |
@rodrimati1992 Good point, can we mandate that the layout of |
Wouldn't those types be compiled separately due to monomorphization and thus be subject to independent optimization choices by the compiler. |
@the8472 that's the question, should different zero-size types affect representation? Josephine is unsafe if different PhantomData have different memory layouts. |
The reasons for wanting to reserve the right to lay out structurally-identical but nominally-different types differently (different types may be used differently incentivizing different layouts, "if you need layout compatibility you can add an attribute") apply here unaltered. I for one do think we should guarantee PhantomData to be irrelevant to layout, but I am also in favor of guaranteeing a lot more than there was consensus for in this thread. |
I think if a special-case were provided for zero-sized types in generics this would result in spooky action at a distance where sometimes you would be guaranteed that a generic type with different parameters are layout-identical until someone changes one of the types to be non-zero-sized and suddenly your layouts are not compatible anymore. Instead you could say something like #[repr(as(Value<T, ()>))]
struct Value<T,C>{
value_0:T,
value_1:usize,
value_2:T,
_marker:PhantomData<fn()->C>,
} Which puts the guarantees or compile time checks at the level where you need them. |
This type parameter passes through multiple layers,and requiring every user of type_level_values to annotate their own types with I would prefer a trait-based solution,in which I can require that two types have compatible memory layout, I forgot to mention that I generate a type alias to pass a PhantomData-like type as the |
Can we say something like we guarantee that the layout for a struct is only dependent on certain properties of its fields? Off the top of my head those are size, alignment and non-zero-ness, but there may be others. This would avoid having to propose an RFC with a language extension. |
That is an option in principle but the current position is that different types with the same properties should allow maximal layout freedom for various kinds of optimization, randomization and similar compiletime decisions. So pretty much any guarantee at all for See the summary in #31 |
Ok,if we go with the annotation based approach,how about something like this:
This attribute would guarantee that the type parameter C would not change the layout of the type regardless of what it is,requiring it to be stored in a zero-sized-type or a type with the same attribute. This attribute could also be detected by derive macros that depend on the type parameter,not changing the layout of the type,and would be usable with any |
The intended effect would be the same. You would just be telling the compiler that layout should behave as if C were set to a fixed value and thus all instances of This would basically be a special case of the |
The effect would be that the attribute would be easier to parse by macros. An alternative if one uses the |
Ah, I have not put much thought into that specific syntax. It could also be expressed in different ways, e.g. |
So, after all this talk about layout guarantees for structs... could we do a little practical exercise and see if rust-lang/rust#54922 if within bounds of the guarantees, or exceeding them? This does some manual layout computation that is supposed to recompute the layout of a |
Quickly chiming in with a +1 for restricting (also cc me) |
I don't know. For example, consider the We have been talking about guaranteeing the layout of homogenous tuples / structs, but I also imagine that this could happen somewhere else, e.g., |
We've called final-comment-period on #31. |
Actually, I see some of the latest comments here -- these are great. Perhaps we can redirect them to #35, which is a subissue on this topic? I'll copy over some of those now. |
Discussion topic about how structs are represented in Rust. Some things to work out:
#[repr(rust)]
struct is laid out(and/or treated by the ABI)?
- e.g., what about different structs with same definition
- across executions of the same program?
#[repr(transparent)]
to being the way to guarantee to other crates that a type with private fields is and will remain a newtype?"#[rust(C)]
guaranteed and what can we say there?The text was updated successfully, but these errors were encountered: