何时进行快速值类型的复制

时间:2014-10-27 18:07:08

标签: swift

在Swift中,当您传递值类型时,请将一个数组说成函数。数组的副本用于要使用的函数。

然而https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ClassesAndStructures.html#//apple_ref/doc/uid/TP40014097-CH13-XID_134的文档也说:

  

上面的描述是指“复制”字符串,数组和   字典。您在代码中看到的行为将始终如同a   副本发生了。但是,Swift只执行实际的副本   绝对有必要这样做的场景。 Swift管理所有   重复值以确保最佳性能,您不应该避免   用于尝试抢占此优化的分配。

那么这是否意味着复制实际上仅在修改传递的值类型时进行?

有没有办法证明这实际上是潜在的行为?

为什么这很重要?如果我创建一个大型的不可变数组并希望将其从函数传递给函数,我当然不希望继续复制它。我是否应该在这种情况下使用NSArrray,或者只要我不尝试操作传入的数组,Swift数组是否正常工作?

现在只要我没有使用var或inout明确地使函数中的变量可编辑,那么该函数无论如何都无法修改数组。那还能复制一份吗?假设另一个线程可以在其他地方修改原始数组(只有它是可变的),在调用该函数时制作副本(但只有在传入的数组是可变的时)。因此,如果原始数组是不可变的并且函数不使用var或inout,则Swift创建副本没有任何意义。对?那么Apple对上述短语的意思是什么?

2 个答案:

答案 0 :(得分:3)

TL; DR:

  

那么这是否意味着复制实际上仅在修改传递的值类型时进行?

是的!

  

有没有办法证明这实际上是潜在的行为?

请参见写时复制优化部分中的第一个示例。

  

在这种情况下我应该只使用NSArrray还是Swift Array可以正常工作     只要我不尝试操纵Array中传递的内容?

如果您将数组作为inout进行传递,那么您将具有按引用传递的语义, 因此显然避免了不必要的复制。 如果您将数组作为常规参数传递, 那么写时复制优化将开始进行,您应该不会注意到任何性能下降 同时仍然受益于NSArray带来的更多类型安全性。

  

现在,只要我不显式使函数中的变量可编辑     通过使用var或inout,该函数无论如何都无法修改数组。     还能复制吗?

您将获得抽象意义上的“副本”。 实际上,由于写时复制机制,基础存储将被共享, 因此避免不必要的复制。

  

如果原始数组是不可变的,并且该函数未使用var或inout,     Swift创建副本没有任何意义。对吧?

确切地说是写复制机制。

  

那么Apple上面的短语是什么意思?

从本质上讲,Apple意味着您不必担心复制值类型的“成本”, Swift会在后台为您优化它。

相反,您应该考虑值类型的语义, 这是您分配或将它们用作参数后立即获得一份副本。 Swift的编译器实际生成的是Swift的编译器业务。

值类型语义

Swift确实将数组视为值类型(与引用类型相对), 以及结构,枚举和大多数其他内置类型 (即那些属于标准库而不是Foundation的库)。 在内存级别,这些类型实际上是不可变的普通旧数据对象(PO​​D), 启用有趣的优化。 实际上,它们通常分配在堆栈上,而不是堆 [1] 上, (https://en.wikipedia.org/wiki/Stack-based_memory_allocation)。 这样,CPU可以非常有效地管理它们, 并在函数退出 [2] 时自动重新分配其内存, 不需要任何垃圾收集策略。

只要赋值或作为函数传递,就复制值。 这种语义有很多优点, 例如避免创建意外的别名, 而且还使编译器更容易保证值的生存期 存储在另一个对象中或由闭包捕获。 我们可以考虑管理好旧的C指针来理解原因有多难。

有人可能会认为这是一个错误的策略, 因为它涉及每一次分配变量或调用函数时复制。 但是可能违反直觉, 复制小型字体通常比通过引用便宜很多。 毕竟,指针通常与整数大小相同。

但是,对于大型集合(即数组,集合和字典)而言,关注点是合法的, 和较小的大型结构 [3] 。 但是编译器有一个技巧来处理这些问题,即写时复制(请参阅稍后)。

mutating

结构可以定义mutating方法, 允许改变结构的字段。 这与值类型仅是不可变的POD并不矛盾, 实际上调用mutating方法只是一个巨大的语法糖 将变量重新分配为与先前值相同的全新值, 除了已突变的字段。 下面的示例说明了这种语义对等:

struct S {
  var foo: Int
  var bar: Int
  mutating func modify() {
    foo = bar
  }
}

var s1 = S(foo: 0, bar: 10)
s1.modify()

// The two lines above do the same as the two lines below:
var s2 = S(foo: 0, bar: 10)
s2 = S(foo: s2.bar, bar: s2.bar)

引用类型语义

与值类型不同,引用类型本质上是在内存级别指向堆的指针。 它们的语义更接近于基于引用的语言, 例如Java,Python或Javascript。 这意味着它们在分配或传递给函数时不会被复制,它们的地址为。 由于CPU不再能够自动管理这些对象的内存, Swift使用参考计数器在后台处理垃圾收集 (https://en.wikipedia.org/wiki/Reference_counting)。

这种语义具有避免复制的明显优势, 因为所有内容都是通过引用分配或传递的。 缺点是存在意外别名的危险, 就像几乎所有其他基于引用的语言一样。

inout

inout参数不过是指向期望类型的读写指针。 对于值类型,这意味着该函数不会获取该值的副本, 但是指向这些值的指针 因此函数内部的突变会影响value参数(因此,inout关键字也将因此而受影响)。 换句话说,这为值类型参数提供了函数上下文中的参考语义:

func f(x: inout [Int]) {
  x.append(12)
}

var a = [0]
f(x: &a)

// Prints '[0, 12]'
print(a)

对于引用类型,它将使引用本身可变, 就像传递的参数是对象地址的地址一样:

func f(x: inout NSArray) {
  x = [12]
}

var a: NSArray = [0]
f(x: &a)

// Prints '(12)'
print(a)

写时复制

写时复制(https://en.wikipedia.org/wiki/Copy-on-write)是一种优化技术,它可以 可以避免不必要地复制可变变量, 这是在所有Swift的内置集合(即数组,集合和字典)上实现的。 当您分配数组(或将其传递给函数)时, Swift不会复制所述数组,而实际上使用了引用。 第二个数组突变后,副本将立即进行。 以下代码段(Swift 4.1)可以证明这种行为:

let array1 = [1, 2, 3]
var array2 = array1

// Will print the same address twice.
array1.withUnsafeBytes { print($0.baseAddress!) }
array2.withUnsafeBytes { print($0.baseAddress!) }

array2[0] = 1

// Will print a different address.
array2.withUnsafeBytes { print($0.baseAddress!) }

实际上,array2不会立即获得array1的副本, 正如事实所指向的一样。 相反,副本是由array2的突变触发的。

此优化还发生在结构的更深层, 这意味着,例如,如果您的收藏是由其他收藏组成的, 后者还将受益于写时复制机制, 如以下代码段(Swift 4.1)所示:

var array1 = [[1, 2], [3, 4]]
var array2 = array1

// Will print the same address twice.
array1[1].withUnsafeBytes { print($0.baseAddress!) }
array2[1].withUnsafeBytes { print($0.baseAddress!) }

array2[0] = []

// Will print the same address as before.
array2[1].withUnsafeBytes { print($0.baseAddress!) }

复制写时复制

实际上,在Swift中实现写时复制机制相当容易, 因为其某些参考计数器API向用户公开。 技巧包括将引用(例如类实例)包装在结构内, 并在变异之前检查该参考是否被唯一引用。 在这种情况下,可以安全地更改包装的值, 否则应将其复制:

final class Wrapped<T> {
  init(value: T) { self.value = value }
  var value: T
}

struct CopyOnWrite<T> {
  init(value: T) { self.wrapped = Wrapped(value: value) }
  var wrapped: Wrapped<T>
  var value: T {
    get { return wrapped.value }
    set {
      if isKnownUniquelyReferenced(&wrapped) {
        wrapped.value = newValue
      } else {
        wrapped = Wrapped(value: newValue)
      }
    }
  }
}

var a = CopyOnWrite(value: SomeLargeObject())

// This line doesn't copy anything.
var b = a

但是,这里有一个输入警告! 阅读isKnownUniquelyReferenced的文档,我们会收到以下警告:

  

如果作为对象传递的实例同时被多个线程访问,     此函数可能仍返回true。     因此,您只能从变异方法中调用此函数     适当的线程同步。

这意味着上述实现不是线程安全的, 由于我们可能会错误地认为包装的对象可以安全地进行突变, 实际上,这样的突变会破坏另一个线程的不变性。 但这并不意味着Swift的写时复制在多线程程序中固有地存在缺陷。 关键是要了解“同时由多个线程访问”的真正含义。 在我们的示例中,如果在多个线程之间共享CopyOnWrite的同一实例,则会发生这种情况, 例如作为共享全局变量的一部分。 被包装的对象将具有线程安全的写时复制语义, 但是拥有它的实例将受到数据争夺的影响。 原因是Swift必须建立唯一所有权 正确评估isKnownUniquelyReferenced [4] , 如果实例的所有者本身在多个线程之间共享,则无法执行此操作。

值类型和多线程

Swift旨在减轻程序员的负担 如苹果博客所述,在处理多线程环境时 (https://developer.apple.com/swift/blog/?id=10):

  

选择值类型而不是引用类型的主要原因之一     是更容易推理代码的能力。     如果您总是得到一个唯一的复制实例,     您可以放心,应用程序的其他部分都不会更改后台数据。     这在多线程环境中特别有用     一个不同的线程可能会在下面改变您的数据。     这会产生难以调试的讨厌的错误。

最终,写时复制机制是一种资源管理优化, 像其他优化技术一样, 在编写代码 [5] 时,您不应该考虑。 相反,应该用更抽象的术语思考 并考虑在赋值或作为参数传递时有效复制的值。


[1] 这仅适用于用作局部变量的值。 用作引用类型(例如,类)的字段的值也存储在堆中。

[2] 可以通过检查产生的LLVM字节码来确认这一点 当处理值类型而不是引用类型时, 但是Swift编译器非常渴望执行恒定的传播, 建立一个最小的例子有点棘手。

[3] Swift不允许结构引用自己, 因为编译器将无法静态计算此类类型的大小。 因此,想到这么大的结构不是很现实 复制它会成为合理的问题。

[4] 顺便说一下,这就是isKnownUniquelyReferenced接受inout参数的原因, 因为这是目前Swift建立所有权的方法。

[5] 尽管传递值类型实例的副本应该是安全的, 有一个公开的问题表明当前的实施存在一些问题 (https://bugs.swift.org/browse/SR-6543)。

答案 1 :(得分:1)

我不知道Swift中的每个值类型是否相同,但是对于Array我很确定它是写时复制的,所以它不会复制它,除非你修改它,正如你所说,如果你把它作为一个常数传递,你就不会冒这个风险。

P.S。在Swift 1.2中,您可以使用新的API在您自己的值类型上实现写时复制