【问题标题】:Does a mutating struct function in swift create a new copy of self?swift 中的变异结构函数是否会创建一个新的 self 副本?
【发布时间】:2017-02-23 16:05:03
【问题描述】:

我喜欢 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)

【问题讨论】:

  • 对于值类型 the existing struct in memory get mutatedself replaced with a new instance 意味着同样的事情。该结构作为堆栈上的一些字节存在。无论您更改结构的单个字段,还是完全分配一个新结构,结构仍然存在于相同的字节中

标签: swift performance struct swift-structs


【解决方案1】:

现在内存中的现有结构会发生变异,还是被新实例自我替换

从概念上讲,这两个选项完全相同。我将使用这个示例结构,它使用 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)时这两种情况会发生什么:

  1. 现有结构已就地变异:

    我们在内存中的结构将如下所示:

    00000000  00000100  00000010  00000000
    <------^  ^------^  ^------^ ^----->
    other    | self.x | self.y | other memory
            ^----------------^
            the p struct
    
  2. Self 被新实例替换:

    我们在内存中的结构将如下所示:

    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) 时,您会看到没有进一步打印。这告诉我们初始化器不是新的。

【讨论】:

  • 好吧,我知道不管怎样,结果状态都是一样的,但是如果 self 被替换不是 self.y 也被重新初始化为相同的值,这意味着浪费时间,或者编译器是聪明的足以让 self.y 一个人呆着,只更新 self.x?
  • 只会改变 x
【解决方案2】:

我觉得值得一看(从相当高的层次)编译器在这里做了什么。如果我们看一下针对以下情况发出的规范 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, @inout Point) -> () {
// %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) -> ()'

(通过运行xcrun swiftc -emit-sil main.swift | xcrun swift-demangle &gt; main.silgen

这里重要的是 Swift 如何处理隐含的self 参数。您可以看到它作为@inout 参数发出,这意味着它将通过reference 传递到函数中。

为了执行x 属性的突变,使用struct_element_addr SIL 指令来查找其地址,然后是Double 的底层_value 属性。然后使用store 指令将生成的双精度值简单地存储回该地址。

这意味着add(_:) 方法能够直接更改内存中px 属性的值,而无需创建任何Point 的中间实例。

【讨论】:

  • 这是有道理的。并回答许多人的误解。感谢您的演示。
【解决方案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: \($0)")
}

p.add(1)

withUnsafePointer(to: &p) {
  print("\(p) has address: \($0)")
}

并在输出中获得:

Point(x: 0.0) 的地址为:0x000000010fc2fb80

Point(x: 1.0) 的地址为:0x000000010fc2fb80

考虑到内存地址没有改变,我敢打赌这个结构是变异的,而不是被替换的。

要完全替换某些东西,你必须使用另一个内存地址,所以在原始内存地址中复制回对象是没有意义的。

【讨论】:

  • 结构不是堆上的对象 :) 你可以根据需要重新分配它们的值,但它们始终是它们开始时相同的静态分配内存部分
  • @Alexander-ReinstateMonica 这样做时,您是只替换结构内的特定变量还是全部替换?
  • @Ricardo 这取决于优化器的聪明程度。如果它知道只有 1 个字段可能已更改,它只会更改一个字段。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-25
  • 1970-01-01
  • 2018-09-20
  • 2016-10-24
  • 1970-01-01
相关资源
最近更新 更多