Memory layout of optional unsafe pointers

While for most types T,
MemoryLayout<T?>.size == MemoryLayout<T>.size + 1,
(ie the wrapping enum adds a byte) i noticed that:

typealias T = UnsafeMutableRawPointer // (or any other Unsafe*Pointer)
MemoryLayout<T>.size == 8
MemoryLayout<T>.stride == 8
MemoryLayout<T>.alignment == 8
MemoryLayout<T?>.size == 8
MemoryLayout<T?>.stride == 8
MemoryLayout<T?>.alignment == 8

How come this is the case?

Can we trust this to stay this way (for the foreseeable future)?

1 Like

The compiler knows to store the nil case as zero pointer (0x00000000). Some enum also behave this way if the compiler can proof that there's an "impossible" case. That said, I don't know the precise criteria, nor do I know if it'll stay that way (though it's very likely).

1 Like

Just out of curiosity, do you have an example?

enum Foo {
    case a(Int8), b(Int8)
}

MemoryLayout<Foo>.size // 2
MemoryLayout<Foo?>.size // 2

There's a lot of room there.

2 Likes

Yes, this is a necessary consequence of how these types are imported from C.

1 Like

Also some interesting cases to show how smart the compiler is at packing data:


enum X {
    case a(UnsafeRawPointer), b
}

MemoryLayout<X>.size // 8
MemoryLayout<X?>.size // 9 0x00000000 got stolen

struct X {
    var a, b: UnsafeRawPointer
}

MemoryLayout<X>.size // 16
MemoryLayout<X?>.size // 16

enum X {
    case a(Int8), b(Int16) 
}

MemoryLayout<X>.size // 3
MemoryLayout<X?>.size // 3
MemoryLayout<X????????>.size // 3

That said, I wouldn't suggest that you rely on this for your custom type.

2 Likes

As noted above, often wrapping enums do not add bytes. While it’s been pointed out that in the pointer case this is necessary, it’s a broader fact that applies throughout Swift. The Swift compiler has awareness of “extra inhabitants”: values of a type that are not in range for those values. It also knows about unused bits of types. This leads to lots of packing efficiencies.

For more I recommend reading the enums article from @jrose’s excellent runtime series.

3 Likes

It's interesting that this topic is coming up now, because I encountered a situation recently where I hoped Swift would optimise this kind of layout, but it didn't.

Basically, if I have a struct containing lots of optionals, I'd like it to coalesce all of the discriminators as a single byte. So given a type:

struct Test {
    var a: Int8?
    var b: Int8?
    var c: Int8?
    var d: Int8?
}

Currently, MemoryLayout<Test>.size == 8 because it's stored as 4 independent optionals, and MemoryLayout<Int8?> is 2. I wish it would be 5 (4 bytes + 1 byte containing all of their discriminators).

IIRC, Swift leaves room for those kind of optimisations to be introduced at some point. I'm a little worried because I think MemoryLayout<T>.offset(of:) might prohibit it.

There's no formal guarantee that a stored property has an addressable offset; if a property is reabstracted, or has observers, for instance, then offset(of:) returns nil for that property. It's likely that a future compiler would require some annotation for properties that have to be addressable, because in the general case it would be better to be able to optimize the layout of structs using overlapping and/or interleaved storage.

9 Likes
Terms of Service

Privacy Policy

Cookie Policy