gpt4 book ai didi

swift - 快速值类型的复制何时发生

转载 作者:行者123 更新时间:2023-11-30 10:40:30 24 4
gpt4 key购买 nike

在Swift中,当您传递值类型时,对函数说一个数组。制作数组副本供功能使用。

但是,https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ClassesAndStructures.html#//apple_ref/doc/uid/TP40014097-CH13-XID_134上的文档还说:


  上面的描述指的是字符串,数组和字符串的“复制”。
  字典。您在代码中看到的行为将始终像
  复制发生了。但是,Swift仅在后面执行实际复制
  绝对必要时在场景中进行。斯威夫特管理所有
  值复制以确保最佳性能,您不应避免
  尝试抢先进行此优化。


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

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

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

现在,只要我没有通过使用var或inout显式使函数中的变量可编辑,那么该函数无论如何都无法修改数组。那它仍然会复制吗?允许另一个线程可以在其他地方修改原始数组(仅在可变的情况下),并在调用该函数时创建一个副本(但仅在传入的数组是可变的情况下)。因此,如果原始数组是不可变的,并且该函数未使用var或inout,则Swift不会创建副本。对?那么,苹果这句话是什么意思呢?

最佳答案

TL; DR:


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


是!


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


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


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


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


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


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


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


确实如此,因此是写时复制机制。


  那么,苹果这句话是什么意思呢?


从本质上讲,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)。

关于swift - 快速值类型的复制何时发生,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56895905/

24 4 0
文章推荐: javascript - Android 上的 HTML