Swift中的struct会不会导致内存问题?

问题描述 投票:3回答:3

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

swift memory-management struct
3个回答
2
投票

从理论上讲,如果你传递非常大的structs导致它们被复制,可能会有内存问题。几点警告/观察:

  1. 在实践中,这很少是一个问题,因为我们经常使用原生的“可扩展”Swift属性,例如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 } } 此示例中的私有Data将采用CoW行为,因此当您制作Foo实例的副本时,大型有效负载将不会被复制,直到您将其变异为止。 最后,您问了一个假设的问题,答案实际上取决于您的大型有效负载所涉及的类型。但对于许多原生的Swift类型,它通常不是问题。
  2. 但是,让我们想象一下,你正处理边缘情况,其中(a)你的组合有效载荷很大; (b)您的struct由不使用CoW的类型组成(即,不是上述可扩展的Swift类型之一); (c)您希望继续享受价值语义(即不转换为具有意外共享风险的参考类型)。在WWDC 2015视频Building Better Apps with Value Types中,他们向我们展示了如何自己使用CoW模式,避免不必要的副本,同时在对象发生变异时仍然执行真正的值类型行为。 考虑: struct Foo { var value0 = 0.0 var value1 = 0.0 var value2 = 0.0 ... } 您可以将它们移动到私有引用类型: 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 } } 然后,您可以更改您的公开值类型以使用此私有引用类型,然后在任何变异方法中实现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 } } } copyIfNeeded方法执行CoW语义,使用isKnownUniquelyReferenced仅复制该有效负载未被唯一引用。 这可能有点多,但它说明了如果您的大型有效载荷尚未使用CoW,如何在您自己的值类型上实现CoW模式。但是,如果(a)您的有效载荷很大,我只建议这样做; (b)您知道相关的有效载荷属性尚未支持CoW,并且(c)您已确定您确实需要该行为。
  3. 如果您碰巧将协议作为类型处理,Swift会在幕后自动使用CoW,当值类型发生变化时,Swift只会生成大值类型的新副本。但是,如果您的多个实例未更改,则不会创建大型有效负载的副本。 有关更多信息,请参阅WWDC 2017视频What’s New in Swift: CoW Existential Buffers: 为了表示未知类型的值,编译器使用我们称为存在容器的数据结构。在存在容器内部有一个用于保存小值的内联缓冲区。我们目前正在重新评估该缓冲区的大小,但对于Swift 4,它仍然是过去的3个单词。如果该值太大而无法放入内联缓冲区,那么它将在堆上分配。 堆存储可能非常昂贵。这就是我们刚刚看到的性能退出的原因。那么我们能做些什么呢?答案是奶牛缓冲液,存在的CoW缓冲液...... ... CoW是“copy on write”的首字母缩写。您之前可能听说过我们之前谈论过这个问题,因为它是具有价值语义的高性能的关键。使用Swift 4,如果某个值太大而无法放入内联缓冲区,则会在堆上分配引用计数。多个存在容器可以共享相同的缓冲区,只要它们只读取它即可。 这避免了大量昂贵的堆分配。如果缓冲区在有多个引用的情况下被修改,则只需要使用单独的分配进行复制。而Swift现在可以完全自动地为您管理复杂性。 有关存在容器和CoW的更多信息,我将向您推荐WWDC 2016视频Understanding Swift Performance

0
投票

这实际上取决于两个主要因素:结构传递的次数,以及结构“保持活动”的时间长度(a.k.a它们是否也被ARC快速清理?)。

可以使用以下公式计算内存消耗总量:mem_usage = count * struct_size

其中count是在任何给定时刻“活着”的结构总数。如果结构仍然存活或快速清理,您需要自己做出判断。


0
投票

是。如果它们在相同的范围内,它将会因为structs在超出范围后被解除分配,所以它们被释放了它们所在的任何基类,但是在范围内具有相同值的太多可以加起来为你制造一个问题所以要小心,this也是一篇很好的文章,详细讨论了这些主题。

你也不能把deinit放在一个结构中直接看这个,但有一个解决方法。你可以创建一个struct,它引用了一个在解除分配时打印出来的类,如下所示:

class DeallocPrinter {
    deinit {
        print("deallocated")
    }
}

struct SomeStruct {
    let printer = DeallocPrinter()
}

func makeStruct() {
    var foo = SomeStruct()
}
makeStruct() // deallocated becasue it escaped the scope

Credits

© www.soinside.com 2019 - 2024. All rights reserved.