Does Swift guarantee the storage order of fields i

2020-02-09 12:06发布

In C, the order in which you define fields in a struct is the order in which they will be instantiated in memory. Taking into account memory alignment, the following struct will have a size of 8 bytes in memory as shown, but only 6 bytes if the fields are reversed as there doesn't need to be any alignment padding.

struct s {
    int32_t a;
    /* 2 bytes of padding to align a 64 bit integer */
    int64_t b;
}

This ordering guarantee is present in C structs, C++ classes (and structs), and Objective-C classes.

Is the order of storage similarly guaranteed for fields in Swift classes and structs? Or (given that the language doesn't support pointers in the same way as the others listed), does the compiler optimally re-arrange them for you at compile-time?

标签: swift
2条回答
Emotional °昔
2楼-- · 2020-02-09 12:25

It seems that the order is guaranteed.

Given the following struct:

struct s {
    var a: UInt32
    var c: UInt8
    var b: UInt64
}

sizeof(s) // 16

Alignment is still happening. There's a lost 8 bits there between c and b.

Of course, it's not clear whether these bits are actually between c & b or just tacked at the end... until we rearrange:

struct s {
    var a: UInt32
    var b: UInt64
    var c: UInt8
}

sizeof(s) // 17

I believe this behavior matches the other languages you mentioned in your question.

查看更多
够拽才男人
3楼-- · 2020-02-09 12:40

Yes, the order of the struct elements in memory is the order of their declaration. The details can be found in Type Layout (emphasis added). Note however the use of "currently", so this may change in a future version of Swift:

Fragile Struct and Tuple Layout

Structs and tuples currently share the same layout algorithm, noted as the "Universal" layout algorithm in the compiler implementation. The algorithm is as follows:

  • Start with a size of 0 and an alignment of 1.
  • Iterate through the fields, in element order for tuples, or in var declaration order for structs. For each field:
    • Update size by rounding up to the alignment of the field, that is, increasing it to the least value greater or equal to size and evenly divisible by the alignment of the field.
    • Assign the offset of the field to the current value of size.
    • Update size by adding the size of the field.
    • Update alignment to the max of alignment and the alignment of the field.
  • The final size and alignment are the size and alignment of the aggregate. The stride of the type is the final size rounded up to alignment.

The padding/alignment is different from C:

Note that this differs from C or LLVM's normal layout rules in that size and stride are distinct; whereas C layout requires that an embedded struct's size be padded out to its alignment and that nothing be laid out there, Swift layout allows an outer struct to lay out fields in the inner struct's tail padding, alignment permitting.

Only if a struct is imported from C then it is guaranteed to have the same memory layout. Joe Groff from Apple writes at [swift-users] Mapping C semantics to Swift

If you depend on a specific layout, you should define the struct in C and import it into Swift for now.

and later in that discussion:

You can leave the struct defined in C and import it into Swift. Swift will respect C's layout.

Example:

struct A {
    var a: UInt8 = 0
    var b: UInt32 = 0
    var c: UInt8 = 0
}

struct B {
    var sa: A
    var d: UInt8 = 0
}

// Swift 2:
print(sizeof(A), strideof(A)) // 9, 12
print(sizeof(B), strideof(B)) // 10, 12

// Swift 3:
print(MemoryLayout<A>.size, MemoryLayout<A>.stride) // 9, 12
print(MemoryLayout<B>.size, MemoryLayout<B>.stride) // 10, 12

Here var d: UInt8 is layed out in the tail padding of var sa: A. If you define the same structures in C

struct  CA {
    uint8_t a;
    uint32_t b;
    uint8_t c;
};

struct CB {
    struct CA ca;
    uint8_t d;
};

and import it to Swift then

// Swift 2:
print(sizeof(CA), strideof(CA)) // 9, 12
print(sizeof(CB), strideof(CB)) // 13, 16

// Swift 3:
print(MemoryLayout<CA>.size, MemoryLayout<CA>.stride) // 12, 12
print(MemoryLayout<CB>.size, MemoryLayout<CB>.stride) // 16, 16

because uint8_t d is layed out after the tail padding of struct CA sa.

As of Swift 3, both size and stride return the same value (including the struct padding) for structures imported from C, i.e. the same value as sizeof in C would return.

Here is a simple function which helps to demonstrate the above (Swift 3):

func showMemory<T>(_ ptr: UnsafePointer<T>) {
    let data = Data(bytes: UnsafeRawPointer(ptr), count: MemoryLayout<T>.size)
    print(data as NSData)
}

The structures defined in Swift:

var a = A(a: 0xaa, b: 0xbbbbbbbb, c: 0xcc)
showMemory(&a)    // <aa000000 bbbbbbbb cc>

var b = B(sa: a, d: 0xdd)
showMemory(&b)    // <aa000000 bbbbbbbb ccdd>

The structures imported from C:

var ca = CA(a: 0xaa, b: 0xbbbbbbbb, c: 0xcc)
showMemory(&ca)   // <aa000000 bbbbbbbb cc000000>

var cb = CB(ca: ca, d: 0xdd)
showMemory(&cb)   // <aa000000 bbbbbbbb cc000000 dd000000>
查看更多
登录 后发表回答