Tuesday, September 8, 2020

Swift Runtime Heap Objects and Type Layout

Jordan Rose:

The way Swift’s flavor of automatic reference counting works is that destruction of the object happens synchronously when the last reference goes away. So while swift_retain ends after updating the reference count, swift_release has to check to see if the object should be destroyed. If the old reference count representation was 0 (remember, it’s a biased representation), then this release is for the last reference, and it’s time to destroy the object. As mentioned above, this information is stored relative to the class metadata, whose address we get by loading the first field of the object.

[…]

The first field of a closure is a function pointer with a special calling convention: it takes the arguments of the closure as well as an additional argument for accessing any captured bindings (parameters, variables, or constants, or explicitly-specified bindings from a capture list). This additional argument is loaded from the second field, and you can think of it as a sort of “anonymous class” that stores the captures. Passing around a closure means retaining and releasing this “captures object”, and it’s destroyed when the reference count hits 0 like any other object.

Jordan Rose:

So what does Swift do? For now, it just does the simple thing: lay the fields out in order, rounding up to alignment boundaries. (This means you can actually save on memory by carefully ordering your fields, although this probably only matters if you have many many instances of your structs.)

[…]

The “Product” example I showed above was a struct that could be laid out completely at compile time. However, that’s not always possible. If the types of First and Second aren’t known at compile time, it’s up to the runtime to figure it out. […] So not only does the runtime need to do type layout, but now you know why it has to be consistent with how the compiler does it.

Previously:

Update (2020-10-16): Jordan Rose:

In Swift, types are represented by unique pointers to structured data, which can be statically or dynamically allocated. This data has a different representation based on what kind of type we’re talking about, so if we’re not sure what kind of type we have, there are only two fields we can access safely: a kind field at offset 0, and a value witness table pointer just before the start of the type metadata. To get at any other information, we have to check the kind first and then cast to the appropriate type.

Jordan Rose:

This time we’re going to be talking about the caches used to unique type metadata (and other things that need uniquing).

Jordan Rose:

Honestly this tour of class metadata is probably more informative than the runtime functions associated with classes, but we’ll go through those too.

Jordan Rose:

With this, we’ve now seen the entire process of creating and initializing class metadata, just like we did with structs. Of course, in many cases the compiler will be able to do much of this work at compile time, but in case it can’t, the Swift runtime is powerful enough to do it all itself.

See also: Swift Unwrapped.

Update (2020-10-23): Jordan Rose:

There’s one other twist on no-payload enums compared to C enums: if you use Swift’s “raw value” support, the representation in memory might still be different from the raw value[…]

[…]

If an enum only has one case that has a payload, the compiler and the runtime conspire collaborate to pick the most efficient layout based on the type of the payload and the number of non-payload cases.

[…]

As quoted above, extra inhabitants are memory representations that are never used for a particular type. The most common of these is “0 will never be a valid pointer”, but there are a few others we can think of, like “100 does not represent a valid Multiplier value” from the enum above. The Swift compiler and runtime are smart enough to use these extra “bit patterns” to represent non-payload cases in a single-payload enum.

Comments RSS · Twitter

Leave a Comment