【问题标题】:Swift enum size when associated value is a reference type关联值是引用类型时的 Swift 枚举大小
【发布时间】:2020-09-15 15:26:57
【问题描述】:

我阅读了有关 Swift 中枚举大小的文档,这是我的理解:

这个简单的只有一个“标签”来区分大小写,默认情况下是一个UInt8值,即small = 0medium = 1等等。所以,Size 的大小是 1 字节,可以用MemoryLayout<Size>.size 验证。我还注意到,如果一个枚举有超过 255 个案例,显然标签大小会升级到 2 个字节。

enum Size {
    case small
    case medium
    case large
}

第二种情况,如果一个枚举有关联的值,它的行为就像一个联合。在这种情况下,枚举大小是标签的大小加上最大关联值的大小。在下面的示例中,大小为 1 字节 + 16 字节(字符串)所以 17 字节,也可以使用MemoryLayout 进行验证。

enum Value {
    case int(Int)
    case double(Double)
    case string(String)
    case bool(Bool)
}

最后一种情况,因为 Swift 是一种安全语言,所以引用在使用标准的非不安全 Swift 代码时总是有效的,即总是指向内存中的一个值。这允许编译器在 T 是引用类型时优化此类枚举:

enum Opt<T> {
    case none
    case some(T)
}

这里T 类型的实例不能被nil (NULL) 使用,因此编译器将这个特殊值用于none 情况,因此Opt 的大小为8 字节而不是T 时的9 字节是引用类型。这种优化是在this SO question about Rust 中提出的,我相信它与 Swift 关于枚举的行为相同。

例如对于这个简单的引用类型,MemoryLayout 返回一个 8 字节的大小:

class Person {
    var name: String

    init(name: String) {
        self.name = name
    }
}

let p = Opt.some(Person(name: "Bob"))  // 8 bytes

问题

我想不通的是这个枚举的大小(仍然是当 T 是引用类型时):

enum Opt<T> {
    case none
    case secondNone
    case some(T)
}

为什么这个也是8字节,根据MemoryLayout

据我了解,它应该是 9 个字节。 NULL 优化是唯一可能的,因为none 可以用 NULL 表示,但在我的示例中,secondNone 没有“第二个”NULL 值,所以这里应该需要一个标签来区分情况。

编译器是否会因此自动将此枚举转换为引用类型(类似于indirect 枚举)?这将解释 8 字节的大小。如何验证最后一个假设?

【问题讨论】:

  • 你在哪里测试这段代码?在真实项目中的发布版本上?如果你在操场上做,那是不可靠的,因为操场没有使用优化。
  • 是的,那是一个游乐场,我将在一个常规项目中对其进行全面优化。
  • 使用-O优化得到了同样的结果。

标签: swift enums size memory-layout


【解决方案1】:

来自Type Layout: Single-Payload Enums

如果数据类型的二进制表示有额外的居民,即具有该类型的大小和对齐方式但不形成该类型的有效值的位模式,则它们用于表示无数据的情况,带有额外的按声明顺序匹配无数据情况的居民,按数值升序排列。

你的例子有更多案例:

enum Opt<T> {
    case a, b, c, d, e, f, g, h, i, j, k
    case l, m, n, o, p, q, r, s, t, u, v
    case some(T)
}

class Person {
    var name: String
    init(name: String) { self.name = name }
}

print(unsafeBitCast(Opt<Person>.a, to: UnsafeRawPointer.self))
// 0x0000000000000000

print(unsafeBitCast(Opt<Person>.b, to: UnsafeRawPointer.self))
// 0x0000000000000002

print(unsafeBitCast(Opt<Person>.v, to: UnsafeRawPointer.self))
// 0x000000000000002a

let p = Person(name: "Bob")
print(unsafeBitCast(Opt.some(p), to: UnsafeRawPointer.self))
// 0x00006030000435d0

显然,0x00x2、...、0x2a 是指针的无效位模式,因此用于其他情况。

确切的算法似乎没有记录,可能需要检查 Swift 编译器源代码。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-11-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多