jsl*_*oop 6 struct memory-management swift
在 Swift 中structs是值类型。如果我有一个包含大量数据的结构(假设)并且我将该结构传递给许多不同的函数,那么每次都会复制该结构吗?如果我同时调用它,那么内存消耗会很高,对吗?
从理论上讲,如果您传递非常大的structs 导致它们被复制,则可能存在内存问题。一些警告/观察:
在实践中,这是很少的问题,因为我们经常使用本机“扩展”斯威夫特的属性,如String,Array,Set,Dictionary,Data,等,以及那些拥有“写入时复制”(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 类型来说,这通常不是问题。
不过,让我们想象一下,您正在处理以下极端情况:(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) 您已确定您确实需要该行为。
如果您碰巧将协议作为类型处理,Swift 会在幕后自动使用 COW 本身,当值类型发生变异时,Swift 只会创建大值类型的新副本。但是,如果您的多个实例未更改,则不会创建大型负载的副本。
有关更多信息,请参阅 WWDC 2017 视频Swift 的新特性:COW Existential Buffers:
为了表示未知类型的值,编译器使用我们称为存在容器的数据结构。在存在容器内有一个内嵌缓冲区来保存小值。我们目前正在重新评估该缓冲区的大小,但对于 Swift 4,它仍然是过去的 3 个单词。如果该值太大而无法放入行内缓冲区,则将其分配在堆上。
堆存储可能非常昂贵。这就是导致我们刚刚看到的性能悬崖的原因。那么我们能做些什么呢?答案是奶牛缓冲区,存在的奶牛缓冲区......
... COW 是“写时复制”的首字母缩写。您之前可能听过我们谈论过这个,因为它是使用值语义实现高性能的关键。在 Swift 4 中,如果一个值太大而无法放入内联缓冲区,它会与引用计数一起分配在堆上。多个存在容器可以共享同一个缓冲区,只要它们只是从中读取。
这避免了大量昂贵的堆分配。如果在有多个引用时修改缓冲区,则只需使用单独的分配复制缓冲区。Swift 现在可以完全自动地为您管理复杂性。
有关存在容器和 COW 的更多信息,我建议您参阅 WWDC 2016 视频了解 Swift 性能。