swift中的变异结构函数是否会创建一个新的self副本?

时间:2017-02-23 16:05:03

标签: swift performance struct swift-structs

我喜欢swift中的值语义,但我担心变异函数的性能。假设我们有以下struct Point { var x = 0.0 mutating func add(_ t:Double){ x += t } }

Point

现在假设我们创建一个var p = Point() p.add(1) 并将其变异为:

self

现在内存中的现有结构变异,或者self = Point(x:self.x+1) 被替换为新实例,如

kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: ingress-nginx
  namespace: default
  labels:
    k8s-addon: ingress-nginx.addons.k8s.io
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: ingress-nginx
        k8s-addon: ingress-nginx.addons.k8s.io
      annotations:
        scheduler.alpha.kubernetes.io/affinity: >
          {
            "podAntiAffinity": {
              "preferredDuringSchedulingIgnoredDuringExecution": [{
                "labelSelector": {
                  "matchExpressions": [
                    { "key": "app", "operator": "In", "values": ["ingress-nginx"] }
                  ]
                },
                "topologyKey": "kubernetes.io/hostname",
                "weight": 100
              }]
            }
          }
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - image: gcr.io/google_containers/nginx-ingress-controller:0.8.3
          name: ingress-nginx
          imagePullPolicy: Always
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
            - name: https
              containerPort: 443
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /healthz
              port: 10254
              scheme: HTTP
            initialDelaySeconds: 30
            timeoutSeconds: 5
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          args:
            - /nginx-ingress-controller
            - --default-backend-service=$(POD_NAMESPACE)/nginx-default-backend
            - --nginx-configmap=$(POD_NAMESPACE)/ingress-nginx

3 个答案:

答案 0 :(得分:5)

  

现在内存中的现有结构变异,或者自我替换为新实例

从概念上讲,这两个选项完全相同。我将使用这个示例结构,它使用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
    
  3. 请注意,两种方案之间没有区别。那是因为为自我分配新值会导致就地变异。 p总是与堆栈上的两个字节的内存相同。为self p分配一个新值只会替换这两个字节的内容,但它仍然是相同的两个字节。

    现在可以是两种情况之间的一个区别,它处理初始化程序的任何可能的副作用。假设这是我们的结构,而不是:

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

答案 1 :(得分:4)

我认为值得一看(从合理的高级别)编译器在这里做什么。如果我们看一下发布的规范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 > main.silgen

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

为了执行x属性的变异,使用struct_element_addr SIL指令来查找其地址,然后使用{{1}的基础_value属性}}。然后使用store指令将结果double存储回该地址。

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

答案 2 :(得分:0)

我这样做了:

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

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

要完全替换某些东西,你必须使用另一个内存地址,因此在原始内存地址中复制该对象毫无意义。