Skip to content

Layout of homogeneous structs #36

@nikomatsakis

Description

@nikomatsakis
Contributor

From #31: If you have homogeneous structs, where all the N fields are of a single type T, can we guarantee a mapping to the memory layout of [T; N]? How do we map between the field names and the indices? What about zero-sized types?

A specific proposal:

  • If all fields have the same type T (ignoring zero-sized types), then the layout will be the same as [T; N] where N is the number of fields
  • If the struct is defined with named fields, the mapping from fields to their indices is undefined (so foo.bar maps an undefined index)
  • If the struct is defined as a tuple struct (or a tuple), then the indices are derived from the definition (so foo.0 maps to [0])

This is basically because it's convenient but also because it's about the only sensible thing to do (unless you imagine we might want to start inserting padding between random fields or something, which connects to #35).

Note that for tuple structs (and tuples), the ordering of (public) fields is also significant for semver and other purposes -- changing the ordering will affect your clients. The same is not true for named fields and so this proposal attempts to avoid making it true.

Activity

the8472

the8472 commented on Oct 11, 2018

@the8472
Member

(unless you imagine we might want to start inserting padding between random fields or something

Would there ever be a reason to automatically insert tail padding for homogenous structs?

hanna-kruppe

hanna-kruppe commented on Oct 11, 2018

@hanna-kruppe

The most plausible reason for not guarantee this would be to raise the alignment of the aggregate to allow copying the type with fewer or more efficient instructions (e.g., align a struct of four u8s to 32 bit and use aligned 32 bit load and store instructions for it).

Gankra

Gankra commented on Oct 11, 2018

@Gankra

I can also imagine two hypothetical but very dubious scenarios to add to the pile:

  • We explicitly want to break code that's relying on the order without an annotation because someone might reorder the fields syntactically without knowing it's important (in some sense, using repr(C) as a "I promise this type will have some kind of stable ABI" annotation)

  • With profile-guided optimization (PGO), it is determined that (say) Foo.7 being first in the type is better due to cache effects.

gnzlbg

gnzlbg commented on Oct 12, 2018

@gnzlbg
Contributor

The most plausible reason for not guarantee this would be to raise the alignment of the aggregate to allow copying the type with fewer or more efficient instructions

I think we should be able to do this for arrays too, and also for homogenous parts of aggregates like struct A { a: u8, x: f32, y: f32, z: f32, w: f32, b: u8 } where it might be worth it to reorder {xyzw} to the front not only to reduce A's size, but also to be able to perform SIMD operations on these fields, which might require raising the alignment of A from 4 to, e.g., 16. I don't think any implementation will do these optimizations in the near future (maybe never), but if leaving the door open for these does not causes many headaches it might still be worth doing nevertheless. We can always provide more guarantees later.

hanna-kruppe

hanna-kruppe commented on Oct 12, 2018

@hanna-kruppe

I think we should be able to do this for arrays too

That would break ABI compatibility with C arrays.

Also, one reason why one might not want to raise the alignment of any aggregate is that it tends to cause extra padding, either internally (in your example struct A, increasing alignment to 16 bytes also increases the size of the struct from 20 bytes to 32 bytes) or externally, i.e., requiring extra padding in an aggregate that contains a field whose alignment requirement was raised.

gnzlbg

gnzlbg commented on Oct 12, 2018

@gnzlbg
Contributor

That would break ABI compatibility with C arrays.

Yes, repr(Rust) arrays would then differ from repr(C) arrays - I don't know how big of a deal this would be.

Also, one reason why one might not want to raise the alignment of any aggregate is that it tends to cause extra padding, either internally (in your example struct A, increasing alignment to 16 bytes also increases the size of the struct from 20 bytes to 32 bytes) or externally, i.e., requiring extra padding in an aggregate that contains a field whose alignment requirement was raised.

Yes, that's a very good reason not to do that. FWIW I wasn't suggesting to do this unconditionally, but rather that we don't have to forbid an implementation from doing this if it deems it worthwhile right now.

Gankra

Gankra commented on Oct 12, 2018

@Gankra

You can't put a repr on a builtin type, so there's no such thing as a "repr(c) array". Also we've long guaranteed their layout, and that ship has sailed.

gnzlbg

gnzlbg commented on Oct 12, 2018

@gnzlbg
Contributor

An alternative would be to asymmetrically require that tuples are layout compatible with arrays, but to not require the layout of tuples and arrays to be identical. That is, arrays would not be layout compatible with tuples.

This would allow the compiler to increase the alignment of homogeneous structs and tuples, and also allow &(T, T, T, T) as &[T; 4]. It would not allow &[T; 4] as &(T, T, T, T) because the alignment of [T; 4] might be smaller than the alignment of (T, T, T, T). An union like U { a: [T; 4], t: (T, T, T, T) } could, however, be used to re-interpret the bytes of an array as a tuple because the union will properly align the array.

AFAICT it would be backwards compatible to, in the future, make the stronger guarantee that the layout of homogeneous tuples and arrays is identical, which would allow &[T; 4] as &(T, T, T, T).

RalfJung

RalfJung commented on Oct 13, 2018

@RalfJung
Member

These ordering issues make me think that for homogeneous non-tuple-structs, while we could guarantee that the layout matches that of an array, we should not guarantee how the fields are mapping to array indices. Getting that from the definition order seems strange to me.

I do not have a strong opinion on this, though.

nikomatsakis

nikomatsakis commented on Oct 18, 2018

@nikomatsakis
ContributorAuthor

@RalfJung

These ordering issues make me think that for homogeneous non-tuple-structs, while we could guarantee that the layout matches that of an array, we should not guarantee how the fields are mapping to array indices. Getting that from the definition order seems strange to me.

This was, I believe, what I meant when I wrote "If the struct is defined with named fields, the mapping from fields to their indices is undefined (so foo.bar maps an undefined index)".

This also seems to address @gankro's first concern:

We explicitly want to break code that's relying on the order without an annotation because someone might reorder the fields syntactically without knowing it's important (in some sense, using repr(C) as a "I promise this type will have some kind of stable ABI" annotation)


With respect to the PGO or SIMD scenarios, if we adopted this proposal, the idea would probably be that one out to explicitly opt in to "non-standard" layouts of this kind (e.g., #[repr(opt)] or something). I think this is probably reasonable -- basically #[repr(Rust)] gives you a compact layout by default that is optimized to do reasonably well on any program with any usage pattern. If you have specific needs (e.g., simd, avoiding false sharing) then those would be something you declare.

hanna-kruppe

hanna-kruppe commented on Oct 18, 2018

@hanna-kruppe

I think this is probably reasonable -- basically #[repr(Rust)] gives you a compact layout by default that is optimized to do reasonably well on any program with any usage pattern. If you have specific needs (e.g., simd, avoiding false sharing) then those would be something you declare.

This has been my position for the longest time, but recently I'm less sure since the scenario outlined in #36 (comment) doesn't really need SIMD or any hypothetical whole-program/profile-guided optimizations to be profitable. It would be nice to be able to raise e.g. the alignment of struct Rgba { r: u8, g: u8, b: u8, a: u8 } to four bytes because doing so would rarely increase data structure sizes (if the struct is private we can even sometimes prove this locally) and on many targets it could make copies of Rgba use 4x fewer instructions.

This is relatively niche compared to reordering to remove internal packing, and considering the nice guarantees it would conflict with, I am not arguing we should reserve that freedom, but I do regret that more than I regret PGO-driven or SIMD-targeted layout changes.

25 remaining items

kornelski

kornelski commented on Jul 5, 2020

@kornelski

Can we get some progress on this? I'd love to have homogeneous tuples defined.

Lokathor

Lokathor commented on Jul 5, 2020

@Lokathor
Contributor

To be clear: nothing defined anywhere in the UCG is actually officially defined and you can't rely on the info you find in the UCG docs until it's actually accepted via RFC.

nikomatsakis

nikomatsakis commented on Jul 27, 2020

@nikomatsakis
ContributorAuthor

I don't think that any guarantee the compiler makes automatically becomes part of semver for crate authors.

I agree with @RalfJung on this, but it's true that we should say one way or the other what we consider to be a breaking change. In other words, it seems quite reasonable to me for the rule to be that:

  • The compiler will not reorder public fields
  • But a crate author may still choose to do so in a minor release

and, therefore, one should not assume that one can transmute or whatever else unless there is an affirmative comment promising not to reorder fields.

This seems fairly consistent to me, in that we generally make our "semver rules", which are more focused on "API" than on "ABI". But as @RalfJung said, I could be persuaded either way I imagine. I think though that we should definitely state explicitly the result, and we should try to be consistent overall.

Lokathor

Lokathor commented on Jul 27, 2020

@Lokathor
the8472

the8472 commented on Jul 27, 2020

@the8472
RalfJung

RalfJung commented on Jul 28, 2020

@RalfJung
Member

If you want to continue discussing semver-related library API guarantees, could you please open a new issue for that discussion? This thread here is about what the compiler guarantees.

the8472

the8472 commented on Jul 28, 2020

@the8472
Member

Ok, see #242

added
S-not-opsemDespite being in this repo, this is not primarily a T-opsem question
and removed
C-open-questionCategory: An open question that we should revisit
on Jun 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-layoutTopic: Related to data structure layout (`#[repr]`)S-not-opsemDespite being in this repo, this is not primarily a T-opsem questionT-lang

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      Participants

      @briansmith@kornelski@nikomatsakis@RalfJung@gnzlbg

      Issue actions

        Layout of homogeneous structs · Issue #36 · rust-lang/unsafe-code-guidelines