swift 中的变异结构函数是否会创建一个新的 self 副本?
Does a mutating struct function in swift create a new copy of self?
我喜欢 swift 中的值语义,但我担心变异函数的性能。假设我们有以下 struct
struct Point {
var x = 0.0
mutating func add(_ t:Double){
x += t
}
}
现在假设我们创建一个 Point
并将其改变为:
var p = Point()
p.add(1)
现在内存中现有的 struct
会发生变异,还是 self
被新实例替换,如
self = Point(x:self.x+1)
Now does the existing struct in memory get mutated, or is self replaced with a new instance
从概念上讲,这两个选项完全相同。我将使用这个示例结构,它使用 UInt8 而不是 Double(因为它的位更容易可视化)。
struct Point {
var x: UInt8
var y: UInt8
mutating func add(x: UInt8){
self.x += x
}
}
假设我创建了这个结构的一个新实例:
var p = Point(x: 1, y: 2)
这会在堆栈上静态分配一些内存。它看起来像这样:
00000000 00000001 00000010 00000000
<------^ ^------^ ^------^ ^----->
other | self.x | self.y | other memory
^----------------^
the p struct
让我们看看当我们调用 p.add(x: 3)
:
时两种情况下会发生什么
现有结构已就地改变:
我们在内存中的结构将如下所示:
00000000 00000100 00000010 00000000
<------^ ^------^ ^------^ ^----->
other | self.x | self.y | other memory
^----------------^
the p struct
自己被替换为一个新实例:
我们在内存中的结构将如下所示:
00000000 00000100 00000010 00000000
<------^ ^------^ ^------^ ^----->
other | self.x | self.y | other memory
^----------------^
the p struct
请注意,这两种情况之间没有区别。那是因为为 self 分配一个新值会导致就地突变。 p
始终是堆栈上相同的两个字节内存。为 p
分配一个新值只会替换这 2 个字节的内容,但它仍然是相同的两个字节。
现在 可能是两种情况之间的一个区别,它处理初始化程序的任何可能的副作用。假设这是我们的结构,而不是:
struct Point {
var x: UInt8
var y: UInt8
init(x: UInt8, y: UInt8) {
self.x = x
self.y = y
print("Init was run!")
}
mutating func add(x: UInt8){
self.x += x
}
}
当您 运行 var p = Point(x: 1, y: 2)
时,您会看到打印了 Init was run!
(如预期的那样)。但是当你 运行 p.add(x: 3)
时,你会看到没有进一步打印。这告诉我们初始化器不是新的。
我这样做了:
import Foundation
struct Point {
var x = 0.0
mutating func add(_ t:Double){
x += t
}
}
var p = Point()
withUnsafePointer(to: &p) {
print("\(p) has address: \([=10=])")
}
p.add(1)
withUnsafePointer(to: &p) {
print("\(p) has address: \([=10=])")
}
并在输出中得到:
Point(x: 0.0) has address: 0x000000010fc2fb80
Point(x: 1.0) has address: 0x000000010fc2fb80
考虑到内存地址没有改变,我敢打赌结构发生了变化,而不是被替换。
要完全替换某些东西,你必须使用另一个内存地址,因此将原始内存地址中的对象复制回没有意义。
我觉得值得(从相当高的层次)看看编译器在这里做了什么。如果我们看一下为以下各项发出的规范 SIL:
struct Point {
var x = 0.0
mutating func add(_ t: Double){
x += t
}
}
var p = Point()
p.add(1)
我们可以看到 add(_:)
方法被发出为:
// Point.add(Double) -> ()
sil hidden @main.Point.add (Swift.Double) -> () :
$@convention(method) (Double, <b>@inout Point</b>) -> () {
// %0 // users: %7, %2
// %1 // users: %4, %3
bb0(%0 : $Double, %1 : $*Point):
// get address of the property 'x' within the point instance.
%4 = struct_element_addr %1 : $*Point, #Point.x, loc "main.swift":14:9, scope 5 // user: %5
// get address of the internal property '_value' within the Double instance.
%5 = struct_element_addr %4 : $*Double, #Double._value, loc "main.swift":14:11, scope 5 // users: %9, %6
// load the _value from the property address.
%6 = load %5 : $*Builtin.FPIEEE64, loc "main.swift":14:11, scope 5 // user: %8
// get the _value from the double passed into the method.
%7 = struct_extract %0 : $Double, #Double._value, loc "main.swift":14:11, scope 5 // user: %8
// apply a builtin floating point addition operation (this will be replaced by an 'fadd' instruction in IR gen).
%8 = builtin "fadd_FPIEEE64"(%6 : $Builtin.FPIEEE64, %7 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64, loc "main.swift":14:11, scope 5 // user: %9
// store the result to the address of the _value property of 'x'.
store %8 to %5 : $*Builtin.FPIEEE64, loc "main.swift":14:11, scope 5 // id: %9
%10 = tuple (), loc "main.swift":14:11, scope 5
%11 = tuple (), loc "main.swift":15:5, scope 5 // user: %12
return %11 : $(), loc "main.swift":15:5, scope 5 // id: %12
} // end sil function 'main.Point.add (Swift.Double) -> ()'
(by 运行 xcrun swiftc -emit-sil main.swift | xcrun swift-demangle > main.silgen
)
这里重要的是 Swift 如何处理隐含的 self
参数。您可以看到它已作为 @inout
参数发出,这意味着它将通过 reference 传递到函数中。
为了执行x
属性的变异,struct_element_addr
SIL instruction is used in order to lookup its address, and then the underlying _value
property of the Double
. The resultant double is then simply stored back at that address with the store
指令
这意味着 add(_:)
方法能够直接更改内存中 p
的 x
属性 的值,而无需创建任何中间实例Point
.
我喜欢 swift 中的值语义,但我担心变异函数的性能。假设我们有以下 struct
struct Point {
var x = 0.0
mutating func add(_ t:Double){
x += t
}
}
现在假设我们创建一个 Point
并将其改变为:
var p = Point()
p.add(1)
现在内存中现有的 struct
会发生变异,还是 self
被新实例替换,如
self = Point(x:self.x+1)
Now does the existing struct in memory get mutated, or is self replaced with a new instance
从概念上讲,这两个选项完全相同。我将使用这个示例结构,它使用 UInt8 而不是 Double(因为它的位更容易可视化)。
struct Point {
var x: UInt8
var y: UInt8
mutating func add(x: UInt8){
self.x += x
}
}
假设我创建了这个结构的一个新实例:
var p = Point(x: 1, y: 2)
这会在堆栈上静态分配一些内存。它看起来像这样:
00000000 00000001 00000010 00000000
<------^ ^------^ ^------^ ^----->
other | self.x | self.y | other memory
^----------------^
the p struct
让我们看看当我们调用 p.add(x: 3)
:
现有结构已就地改变:
我们在内存中的结构将如下所示:
00000000 00000100 00000010 00000000 <------^ ^------^ ^------^ ^-----> other | self.x | self.y | other memory ^----------------^ the p struct
自己被替换为一个新实例:
我们在内存中的结构将如下所示:
00000000 00000100 00000010 00000000 <------^ ^------^ ^------^ ^-----> other | self.x | self.y | other memory ^----------------^ the p struct
请注意,这两种情况之间没有区别。那是因为为 self 分配一个新值会导致就地突变。 p
始终是堆栈上相同的两个字节内存。为 p
分配一个新值只会替换这 2 个字节的内容,但它仍然是相同的两个字节。
现在 可能是两种情况之间的一个区别,它处理初始化程序的任何可能的副作用。假设这是我们的结构,而不是:
struct Point {
var x: UInt8
var y: UInt8
init(x: UInt8, y: UInt8) {
self.x = x
self.y = y
print("Init was run!")
}
mutating func add(x: UInt8){
self.x += x
}
}
当您 运行 var p = Point(x: 1, y: 2)
时,您会看到打印了 Init was run!
(如预期的那样)。但是当你 运行 p.add(x: 3)
时,你会看到没有进一步打印。这告诉我们初始化器不是新的。
我这样做了:
import Foundation
struct Point {
var x = 0.0
mutating func add(_ t:Double){
x += t
}
}
var p = Point()
withUnsafePointer(to: &p) {
print("\(p) has address: \([=10=])")
}
p.add(1)
withUnsafePointer(to: &p) {
print("\(p) has address: \([=10=])")
}
并在输出中得到:
Point(x: 0.0) has address: 0x000000010fc2fb80
Point(x: 1.0) has address: 0x000000010fc2fb80
考虑到内存地址没有改变,我敢打赌结构发生了变化,而不是被替换。
要完全替换某些东西,你必须使用另一个内存地址,因此将原始内存地址中的对象复制回没有意义。
我觉得值得(从相当高的层次)看看编译器在这里做了什么。如果我们看一下为以下各项发出的规范 SIL:
struct Point {
var x = 0.0
mutating func add(_ t: Double){
x += t
}
}
var p = Point()
p.add(1)
我们可以看到 add(_:)
方法被发出为:
// Point.add(Double) -> ()
sil hidden @main.Point.add (Swift.Double) -> () :
$@convention(method) (Double, <b>@inout Point</b>) -> () {
// %0 // users: %7, %2
// %1 // users: %4, %3
bb0(%0 : $Double, %1 : $*Point):
// get address of the property 'x' within the point instance.
%4 = struct_element_addr %1 : $*Point, #Point.x, loc "main.swift":14:9, scope 5 // user: %5
// get address of the internal property '_value' within the Double instance.
%5 = struct_element_addr %4 : $*Double, #Double._value, loc "main.swift":14:11, scope 5 // users: %9, %6
// load the _value from the property address.
%6 = load %5 : $*Builtin.FPIEEE64, loc "main.swift":14:11, scope 5 // user: %8
// get the _value from the double passed into the method.
%7 = struct_extract %0 : $Double, #Double._value, loc "main.swift":14:11, scope 5 // user: %8
// apply a builtin floating point addition operation (this will be replaced by an 'fadd' instruction in IR gen).
%8 = builtin "fadd_FPIEEE64"(%6 : $Builtin.FPIEEE64, %7 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64, loc "main.swift":14:11, scope 5 // user: %9
// store the result to the address of the _value property of 'x'.
store %8 to %5 : $*Builtin.FPIEEE64, loc "main.swift":14:11, scope 5 // id: %9
%10 = tuple (), loc "main.swift":14:11, scope 5
%11 = tuple (), loc "main.swift":15:5, scope 5 // user: %12
return %11 : $(), loc "main.swift":15:5, scope 5 // id: %12
} // end sil function 'main.Point.add (Swift.Double) -> ()'
(by 运行 xcrun swiftc -emit-sil main.swift | xcrun swift-demangle > main.silgen
)
这里重要的是 Swift 如何处理隐含的 self
参数。您可以看到它已作为 @inout
参数发出,这意味着它将通过 reference 传递到函数中。
为了执行x
属性的变异,struct_element_addr
SIL instruction is used in order to lookup its address, and then the underlying _value
property of the Double
. The resultant double is then simply stored back at that address with the store
指令
这意味着 add(_:)
方法能够直接更改内存中 p
的 x
属性 的值,而无需创建任何中间实例Point
.