如果大量传递,Swift 中的 struct 会导致内存问题吗?

jsl*_*oop 6 struct memory-management swift

在 Swift 中structs是值类型。如果我有一个包含大量数据的结构(假设)并且我将该结构传递给许多不同的函数,那么每次都会复制该结构吗?如果我同时调用它,那么内存消耗会很高,对吗?

Rob*_*Rob 6

从理论上讲,如果您传递非常大的structs 导致它们被复制,则可能存在内存问题。一些警告/观察:

  1. 在实践中,这是很少的问题,因为我们经常使用本机“扩展”斯威夫特的属性,如StringArraySetDictionaryData,等,以及那些拥有“写入时复制”(COW)的行为。这意味着,如果您复制 ,struct则不必复制整个对象,而是它们在内部采用类似引用的行为来避免不必要的重复,同时仍保留值类型语义。但是如果你改变了有问题的对象,只有这样才能制作一个副本。

    这是两全其美的方式,您可以享受价值语义(无意外共享),而无需为这些特定类型生成不必要的重复数据。

    考虑:

    struct Foo {
        private var data = Data(repeating: 0, count: 8_000)
    
        mutating func update(at: Int, with value: UInt8) {
            data[at] = value
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    Data本示例中的私有将采用 COW 行为,因此当您复制 的实例时Foo,大型有效负载将不会被复制,直到您对其进行变异。

    最重要的是,您提出了一个假设性问题,答案实际上取决于您的大型有效载荷中涉及的类型。但是对于许多原生 Swift 类型来说,这通常不是问题。

  2. 不过,让我们想象一下,您正在处理以下极端情况:(a) 组合的有效载荷很大;(b) 你struct是由不使用 COW 的类型组成的(即,不是上述可扩展的 Swift 类型之一);(c) 你想继续享受价值语义(即不转向有意外共享风险的引用类型)。在 WWDC 2015 视频“用值类型构建更好的应用程序”中,他们向我们展示了如何自己使用 COW 模式,避免不必要的副本,同时在对象发生变异时仍然强制执行真正的值类型行为。

    考虑:

    struct Foo {
        var value0 = 0.0
        var value1 = 0.0
        var value2 = 0.0
        ...
    }
    
    Run Code Online (Sandbox Code Playgroud)

    您可以将这些移动到私有引用类型中:

    private class FooPayload {
        var value0 = 0.0
        var value1 = 0.0
        var value2 = 0.0
        ...
    }
    
    extension FooPayload: NSCopying {
        func copy(with zone: NSZone? = nil) -> Any {
            let object = FooPayload()
            object.value0 = value0
            ...
            return object
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    然后,您可以更改公开的值类型以使用此私有引用类型,然后在任何变异方法中实现 COW 语义,例如:

    struct Foo {
        private var _payload: FooPayload
    
        init() {
            _payload = FooPayload()
        }
    
        mutating func updateSomeValue(to value: Double) {
            copyIfNeeded()
    
            _payload.value0 = value
        }
    
        private mutating func copyIfNeeded() {
            if !isKnownUniquelyReferenced(&_payload) {
                _payload = _payload.copy() as! FooPayload
            }
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    copyIfNeeded方法执行 COW 语义,isKnownUniquelyReferenced仅在未唯一引用该有效负载时才进行复制。

    这可能有点多,但它说明了如果您的大型有效负载尚未使用 COW,则如何在您自己的值类型上实现 COW 模式。不过,我只建议这样做,如果 (a) 您的有效载荷很大;(b) 您知道相关的有效载荷属性尚不支持 COW,并且 (c) 您已确定您确实需要该行为。

  3. 如果您碰巧将协议作为类型处理,Swift 会在幕后自动使用 COW 本身,当值类型发生变异时,Swift 只会创建大值类型的新副本。但是,如果您的多个实例未更改,则不会创建大型负载的副本。

    有关更多信息,请参阅 WWDC 2017 视频Swift 的新特性:COW Existential Buffers

    为了表示未知类型的值,编译器使用我们称为存在容器的数据结构。在存在容器内有一个内嵌缓冲区来保存小值。我们目前正在重新评估该缓冲区的大小,但对于 Swift 4,它仍然是过去的 3 个单词。如果该值太大而无法放入行内缓冲区,则将其分配在堆上。

    堆存储可能非常昂贵。这就是导致我们刚刚看到的性能悬崖的原因。那么我们能做些什么呢?答案是奶牛缓冲区,存在的奶牛缓冲区......

    ... COW 是“写时复制”的首字母缩写。您之前可能听过我们谈论过这个,因为它是使用值语义实现高性能的关键。在 Swift 4 中,如果一个值太大而无法放入内联缓冲区,它会与引用计数一起分配在堆上。多个存在容器可以共享同一个缓冲区,只要它们只是从中读取。

    这避免了大量昂贵的堆分配。如果在有多个引用时修改缓冲区,则只需使用单独的分配复制缓冲区。Swift 现在可以完全自动地为您管理复杂性。

    有关存在容器和 COW 的更多信息,我建议您参阅 WWDC 2016 视频了解 Swift 性能