Guarantee (in-memory) tuple layout…or don't

Background

  • Swift has never guaranteed the in-memory layout (size and alignment) of structs and classes, with only a few exceptions:

    • A struct with one element has the same layout as that element.
    • Class instances have two words (Int-sized and -aligned fields) at the start of the allocation, so class instances are always word-aligned.
    • Structs defined from C will have the same size and alignment as they do in C (as one would hope).
  • Tuples have always had their own guarantee: if all the elements are the same type, they will be laid out in order by stride (size rounded up to alignment), just like a fixed-sized array in C. This applies even if the elements are Swift types, which has come up when trying to manipulate elements in tuples through pointers to the whole tuple. The most recent citation for this is this year's WWDC's "Safely manage pointers in Swift":

    Swift's implementation does guarantee that tuples whose elements are all the same type are laid out in a standard pattern, one value after another, according to the stride of the element type.

    (Thanks for looking that up and sending it my way, @Lily_Ballard.)

  • There's an additional constraint with ABI stability: different compilers must be able to directly manipulate @frozen structs that come from library-evolution-enabled modules, so the layout of these structs is guaranteed as well. (This layout happens to be "lay out fields in order, rounding up only for alignment" on Apple platforms.) This does not extend to non-frozen structs or to structs in non-library-evolution-enabled modules, because there's no agreement that has to happen across versions of the compiler (and runtimes) about the layout of these structs.

  • The same does apply to tuples (as part of ABI stability), which are basically treated as ad hoc frozen structs. In theory tuples only referenced from a single module could use a totally different in-memory layout from everywhere else, but you'd have to prove that there was no access to those tuples from somewhere else, and that's probably not worth it.

  • All of this only applies to in-memory representations, which means "those you could theoretically get a pointer to". If I pass a tuple by value, the compiler is free to split it up across multiple machine registers; if I have a tuple local variable, the compiler can throw away fields that are written to but never read. (You can't observe the memory layout of something you don't get a pointer to, MemoryLayout notwithstanding, so don't worry about it!)

The Question: Are non-homogeneous tuples guaranteed to use the in-order, rounding-up layout that frozen structs on Apple platforms use?

There are past comments from @John_McCall that imply the answer is yes...

...but I would like this to be written down somewhere less ephemeral than old forum threads. On the other hand, if that answer is being retracted (i.e. only applied to earlier versions of Swift), I would like that to be written down somewhere. So I'm putting this in Evolution/Pitches because a proposal is the closest the community has to "request official word from the core team".

Why does this matter?

I'm not sure it does.

  • Tuple layout still isn't going to be the same as C struct layout because even on platforms where C uses the same layout algorithm (most of them), C doesn't differentiate between size and stride. (Instead, the size also gets rounded up to the alignment.)

  • Having a way to control memory layout of Swift types is something I think we'll want to do eventually, but others have disagreed, saying that manual type layout should be left to C headers (perhaps using compiler extensions like #pragma pack). I think both camps would agree that controlling memory layout by always using tuple types would be a bad solution, though.

  • One "feature" of in-order layout is that pointers to later fields compare greater than pointers to earlier fields. I have never seen this be useful for anything, however.

  • The idea of automatically packing fields together comes up now and again (for fields with Bool or small-enum types). I think actually making that change for structs would be quite the upheaval due the existence of MemoryLayout.offset(of:) (if not technically source-breaking or ABI-breaking). But even sidestepping that, we already have the homogeneous tuple guarantee—that (Bool, Bool, Bool) is equivalent to bool[3] in C—and it seems weird to me that (Bool, Bool, Bool, Int8) would make such a different layout choice.

Even without motivation, though, the question of tuple layout resurfaces every now and then, and people are also surprised to learn that struct and class layout are not guaranteed. (I wish we hadn't stuck with something so close to C layout on our platforms, so it would have been more obvious!) So I think it's worth writing down what is and isn't guaranteed about tuples on non-ABI-stable platforms.

  • Does the core team have any comments?
  • Does anyone have any use cases for knowing the layout of tuples, specifically?
25 Likes

This seems like an entirely reasonable "proposal".

3 Likes

I would like to see more explicit guarantees made here as well.

1 Like

+1

I’d like to go further and enable guaranteed layout for structs as well.

4 Likes

I’m actually not sure I do want it for structs. In-order layout isn’t a very good algorithm for non-generic structs, and alignment isn’t something most people think about when writing their types. And why should they? Everything else in Swift only depends on order if you specifically say so.

Generic layout has a lot more trade-offs. Is it best to put the non-generic fields first, so that they have compile-time offsets? Or is it better to defer such decisions to run time, so that the instantiated structs can be packed better?

Anyway, with tuples it seems good to pin this down, and with structs I’d love to have Swift do something better.

4 Likes

Opt-in is fine.

2 Likes

Should the _Buffer32 and _Buffer72 types store tuples?

I am 100% in favor of writing down what the current behavior is. As you say, it's not clear if guaranteeing the layout matters.

@lukasa what kind of explicit guarantees would you like to be see made in this area? More concretely, what kinds of questions should be answered by said guarantees but are not answered clearly enough today? Would a description of the existing behavior be "good enough"?

As a starting point, writing down "what we do today" on Apple platforms has the effect of providing a guarantee because of the ABI stability of those platforms. For now, I think this is sufficient to satisfy my needs.

2 Likes

This is only sufficient for types that cross module boundaries. Private or internal types can use different non-stable layouts.

Writing down “what we do today” is still important, but we do need guarantees about what behavior will be guaranteed in the future. For example, the fact that homogenous tuples are laid out like a C array is not officially documented, it’s only mentioned in WWDC talks and the forums. We need actual documentation on these behaviors.

4 Likes

I would also prefer to write it down, but I believe source stability plus the fact that the clang importer maps between these types constitutes a forward guarantee.

The Clang importer behavior is written down here (thanks @gribozavr): https://github.com/apple/swift/blob/master/docs/HowSwiftImportsCAPIs.md#fixed-size-arrays

(There are plenty of importer quirks not mentioned there, such as when you get Bool vs. ObjCBool, but in this case we have some documentation.)

2 Likes

Source stability doesn't affect usage of undocumented behavior. There is no official guarantee today of the layout of types, nor should there be in most cases (because the layout today might not be the optimal layout going forward, and because for most types changing the layout of a private/internal type doesn't affect anything).

As for the clang importer, it's always free to add extra hidden attributes to types in order to guarantee C-compatible layout. This is pretty much required by the fact that C structs imported in Swift need to match C layout, but Swift structs don't (and AIUI C structs get padded to their stride, but Swift structs don't, so it's already doing this today for structs).

2 Likes

I may be a little confused: guaranteeing the layout of heterogeneous tuples on non-ABI stable platforms is just a mini ABI-stability, isn't it? What's the benefit of introducing stability for heterogeneous tuples but not for anything else?

In general, I do support having some guaranteed layout algorithms for other types (like structs). I just don't understand why people need this specific thing to have a guaranteed layout. Especially if it doesn't match the C struct layout, meaning it's kind of a Swift-arbitrary layout anyway.

Who says that manual type layout should not be possible in pure Swift? I'd be interested to hear their arguments.

There's 3 parts to this:

  1. We need a guarantee of the layout of homogenous tuples. This is important for C compatibility so we can pass a pointer to stack-allocated storage, but this is also potentially useful from Swift too (e.g. an API that provides an UnsafeRawBufferPointer and might opt to construct that pointer from the stack instead of heap memory). Homogenous tuples do currently match C layout, but this is not officially documented.
  2. Heterogenous tuples are less obviously useful to have guaranteed layout as they're not going to be used to construct C arrays or arbitrary byte buffers. But @jrose found a use for this, which is to allow for constructing Swift types that match the layout of C structs, as he was under the impression from the forum posts linked upthread that all tuples had guaranteed layout, not just homogenous ones.
  3. Structs are similar to homogenous tuples here, in that it might be nice to define a struct in Swift that can be consumed from C, perhaps because you're satisfying an ABI interface (either using @_cdecl or using @convention(c) blocks).

I would very much like to be able to do manual type layout in pure Swift. I don't think "just import a C header if you need this" is a great solution, especially because when writing libraries today you cannot import from a C header without exposing that same header to clients of your API (this is definitely true with Xcode, I'm not sure yet if SPM has a workaround for this).

Even without C compatibility, manual type layout is potentially useful when dealing with large structs that straddle cache lines, as you may know better than the compiler which data is accessed together and should reside on the same cache line. This is particularly important if we ever get atomics (pretty please!)

I think my ideal solution right now looks like:

  • Homogenous tuples are guaranteed to match C array layout. The only penalty here is the tuple will have its size rounded up to match its stride, which does not seem like a big issue.
  • Heterogenous tuples and structs do not guarantee layout except when using @frozen with library evolution on an ABI-stable platform (which AIUI is a condition today where they are guaranteed to match C layout). And these types should use a smart layout selection to minimize padding.
  • An attribute, maybe @convention(c), can be applied to struct and tuple types to request that they match C layout.
  • For that matter, we'll want an attribute that is equivalent to C's __attribute__((__packed__)), both so we can replicate packed C structs, and so we can request fixed layout but without extra padding (for when we really really want to control memory layout). This could be a separate attribute, or maybe instead of using @convention(c) we could do something like @repr(C) and @repr(packed) (the name repr here is chosen to match Rust's #[repr(C)] cfg).
2 Likes

To be clear, I don’t endorse this as the right solution, and I should probably edit my blog post to say that. The right answer for what I did would be to define all those types in C.

1 Like

All tuples are effectively frozen, by the way, since their fields are both ordered and statically known. And this is not quite C layout because it still keeps size and stride separate.

Note that existing tuples do not use C layout either, even for homogeneous types, meaning that there will not be tail padding…but it’s “okay” because anything you can expose to C is made of types imported from C or types whose size is already a multiple of alignment. Something to be careful of if manipulating tuples of Swift types from C, however…!

I don’t think this is ever a good idea, honestly. Proper use of packed structs mean no pointers to members, which breaks plenty of low-level optimizations. Everything has to go through memcpy-equivalents. (I’m not even sure how it would work for non-bitwise-takable types like ObjC weak pointers.) The right way to go for packed structures is probably some kind of @Unaligned property wrapper so that it’s very obvious how it’s going to affect performance and correctness. (And that’s something that can be written today, if unsafely because you can’t enforce triviality).

I do support a @repr(c), though, and possibly a @repr(inOrderWithoutPadding) that’ll complain at you for any wasted bytes. That would handle probably 97% of the use cases for explicit struct layout.

2 Likes

If C had such an attribute, and its behavior was fully defined, I might support this. But this is a GCC/Clang extension, and the behavior is underspecified, and doesn't match the behavior of similar attributes in other compilers. Given those caveats, I don't think there's a rationale for including this in Swift.

Even in C, even when restricting to GCC and Clang, my experience is that using this attribute is often a bad idea; it's usually better to define the pack and unpack operations explicitly in code and avoid the surprises that arise from how compilers handle unaligned access on some platforms.

This is, IMO, the only good argument for explicit layout control in Swift (as opposed to importing a C struct definition), but it's a really good argument.

1 Like

So I'm happy to come back with @repr(c) and/or @repr(inOrderWithoutPadding) in a separate thread, but how to move forward with the homogeneous and heterogeneous tuple guarantees? (The main difference for homogeneous tuples is whether there's tail padding when the element type is a Swift type.) Would the core team want an actual proposal format for this?

Terms of Service

Privacy Policy

Cookie Policy