Enum layout is this whole complicated thing that I can't get into right now.
Notices by John McCall (rjmccall@hachyderm.io)
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:42 JST John McCall -
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:42 JST John McCall Swift in practice always uses in-order layout for structs and tuples: the first stored property/element goes at offset 0, and then N+1 goes at offset (offset of N + size (not stride) of N + any alignment padding for N+1). This is guaranteed for tuples, but not for structs, where we reserve the right to play bit-packing tricks in the future.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:41 JST John McCall Essentially, the copies emerge as necessary in order to fulfill the high-level semantics without imposing a Rust-like requirement to always be explicit about ownership when writing code. You can certainly argue about whether this is the right thing to do! I think it is, but it's definitely a trade-off point in the design. Regardless, this is how it works.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:41 JST John McCall 15:00 - In some ways this presentation of ownership is backwards from how you should usually think of it — you want to think semantically about how you're using values, not what particular contexts require. But this reverse thinking is necessary in order to understand when and why Swift inserts copies.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:40 JST John McCall 17:10 - Swift supports passing computed storage as an inout argument, and this works by calling the getter, putting that value into temporary storage, doing the stored-storage ownership dance described here, and then passing the new value of the temporary back to the setter.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:40 JST John McCall 15:25 - Another major example of a context that always consumes a value is a return (or a throw). This includes a getter, of course, which is really just a function that returns a value. If you read a property that has a getter than just returns the value of a different property, there is necessarily a copy there. The optimizer may be able to eliminate that copy, of course, if it can do IPO with the getter.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:39 JST John McCall Swift sometimes needs to access a stored property abstractly. For example, a public property of a type from a module built with library evolution (like of Apple's OS) generally can't be assumed to be a stored property by clients outside of the defining module. You might expect that this means that Swift implicitly generates a getter and setter for the property, which would mean that mutations of it would involve an implicit copy, as described above.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:39 JST John McCall People sometimes describe this overall model as if the value is copied in and then copied back out, and I get why they think of it that way. If you want to understand the performance, though, you need to understand it in this somewhat more precise way.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:38 JST John McCall In fact, this is not true. Swift performs these mutations by calling a coroutine that yields access to mutable storage. The mutation is done directly to that storage, and then the coroutine is resumed so it can clean up the access.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:37 JST John McCall Swift does have to use getters and setters directly for mutations of properties that it has to access through an ObjC-style interface, since that interface does not include this `_modify` coroutine.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:37 JST John McCall When the property is actually implemented as a stored property, this coroutine simply directly yields the normal storage, and so no copy is required. Only when the property is implemented as computed property does the coroutine use the getter-temporary-setter pattern.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:36 JST John McCall 18:25 - The most obvious missing language feature here is a `borrow` operator, which I expect to be a relatively straightforward addition to the language. But we also ought to be able to make stronger guarantees about automatically borrowing in many more situations, like when you pass the value of a local variable that definitely is not being simultaneously mutated or consumed.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:36 JST John McCall 17:45 - `print` is actually a really bad example here. The current `print` cannot take a value like this without copying it because it actually takes arguments of type `Any`, and constructing any `Any` requires consuming a value representation. This is one of several tragic things about the current definition of `print` that we'd like to fix.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:35 JST John McCall I think Swift's decision here is the right one for most code, but not having some of these features in place already does make the story feel a little incomplete.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:35 JST John McCall Fundamentally, Swift is making a usability decision with a performance trade-off — by default, we assume it's better to implicitly copy something than to force the programmer to prove that it isn't simultaneously accessed. Again, you can certainly argue that that's the wrong decision to make.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:34 JST John McCall 22:50 - As mentioned above, the special case here is that the first stored property of a struct always has an offset of zero, even if it's dynamically-sized.
There's another special case we *could* do: because Swift caps type alignment to 16 bytes, if the previous stored property happens to end at an offset that's a multiple of 16, we ought to know that the next property always starts there without any alignment padding. But I don't believe we currently take advantage of this.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:34 JST John McCall 22:00 - The fact that Swift already supports the complexity of dynamically-sized types for all these abstraction reasons actually means we're also well set up to support them for other reasons. For example, people have been talking about how to support fixed-size arrays in Swift for awhile; if we add that feature, I think we could relatively easily go further and support non-constant bounds, and we wouldn't have to restrict where they appear the way that e.g. C99 does with VLAs.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:33 JST John McCall 25:15 - In practice, most async functions will also have additional implicit potential suspension points related to scheduling: whenever you enter the function (either in the prologue or after returning from a call), the function will potentially suspend in order to make sure it's running on the right executor. The optimizer will remove these suspensions if the function does nothing of significance before it reaches a different suspension point, such as a call or return.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:33 JST John McCall 24:10 - C compilers do something similar to this with alloca() and VLAs.
-
Embed this notice
John McCall (rjmccall@hachyderm.io)'s status on Friday, 14-Jun-2024 10:33:32 JST John McCall But the idea is still the same; you just get a few more partial functions.