Does a mutating struct function create a new copy of self?
Asked Answered
E

4

14

I like value semantics in Swift but I am worried about the performance of mutating functions. Suppose we have the following struct:

struct Point {
   var x = 0.0
   mutating func add(_ t:Double){
      x += t
   }
}

Now suppose we create a Point and mutate it as so:

var p = Point()
p.add(1)

Now does the existing struct in memory get mutated, or is self replaced with a new instance as in:

self = Point(x:self.x+1)
Elea answered 23/2, 2017 at 16:5 Comment(1)
For value types the existing struct in memory get mutated and self replaced with a new instance mean the same thing. The struct exists as some number of bytes on stack. Whether you change a single field of the struct, or assign a new struct altogether, the struct still exists in those same bytesFeathers
F
19

Now does the existing struct in memory get mutated, or is self replaced with a new instance

Conceptually, these two options are exactly the same. I'll use this example struct, which uses UInt8 instead of Double (because its bits are easier to visualize).

struct Point {
    var x: UInt8
    var y: UInt8

    mutating func add(x: UInt8){
       self.x += x
    }
}

and suppose I create a new instance of this struct:

var p = Point(x: 1, y: 2)

This statically allocates some memory on the stack. It'll look something like this:

00000000  00000001  00000010  00000000
<------^  ^------^  ^------^ ^----->
other     |self.x | self.y | other memory
          ^----------------^
          the p struct

Let's see what will happen in both situations when we call p.add(x: 3):

  1. The existing struct is mutated in-place:

    Our struct in memory will look like this:

     00000000  00000100  00000010  00000000
     <------^  ^------^  ^------^ ^----->
     other    | self.x | self.y | other memory
             ^----------------^
             the p struct
    
  2. Self is replaced with a new instance:

    Our struct in memory will look like this:

     00000000  00000100  00000010  00000000
     <------^  ^------^  ^------^ ^----->
     other    | self.x | self.y | other memory
             ^----------------^
             the p struct
    

Notice that there's no difference between the two scenarios. That's because assigning a new value to self causes in-place mutation. p is always the same two bytes of memory on the stack. Assigning self a new value to p will only replace the contents of those 2 bytes, but it'll still be the same two bytes.

Now there can be one difference between the two scenarios, and that deals with any possible side effects of the initializer. Suppose this is our struct, instead:

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
    }
}

When you run var p = Point(x: 1, y: 2), you'll see that Init was run! is printed (as expected). But when you run p.add(x: 3), you'll see that nothing further is printed. This tells us that the initializer is not anew.

Feathers answered 23/2, 2017 at 16:22 Comment(4)
Ok I understand that the resulting state will be the same regardless, but if self is replaced is not self.y also getting reinitialized to the same value, meaning wasted time, or is the compiler smart enough to leave self.y alone and only update self.x?Elea
It'll only change xFeathers
So you mean that mutating does not cause any such memory issue in Swift. Right? I have seen developer trying to update only 1 property of struct by creating a new Struct and updating only 1 property. So is it good that rather than this we should using mutating to initialize 1 property.Trinette
@Trinette Either works well, though people tend to steer away from mutability by default, so you'll see a lot of functions like func with(...) -> Self which return a new copy of self with some fields changed. In general, there's not much performance concern. If it's an issue, profile itFeathers
G
15

I feel it's worth taking a look (from a reasonably high-level) at what the compiler does here. If we take a look at the canonical SIL emitted for:

struct Point {
    var x = 0.0
    mutating func add(_ t: Double){
        x += t
    }
}

var p = Point()
p.add(1)

We can see that the add(_:) method gets emitted as:

// 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) -> ()'

(by running xcrun swiftc -emit-sil main.swift | xcrun swift-demangle > main.silgen)

The important thing here is how Swift treats the implicit self parameter. You can see that it's been emitted as an @inout parameter, meaning that it'll be passed by reference into the function.

In order to perform the mutation of the x property, the 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 instruction.

What this means is that the add(_:) method is able to directly change the value of p's x property in memory without creating any intermediate instances of Point.

Granitite answered 23/2, 2017 at 17:12 Comment(1)
That's what makes sense. And answer the misconception many are having. Thanks for the demonstration.Hydrolyze
P
1

I did this:

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)")
}

and obtained in output:

Point(x: 0.0) has address: 0x000000010fc2fb80

Point(x: 1.0) has address: 0x000000010fc2fb80

Considering the memory address has not changed, I bet the struct was mutated, not replaced.

To replace completely something, you have to use another memory address, so it's pointless to copy back the object in the original memory address.

Posner answered 23/2, 2017 at 16:24 Comment(3)
Structs aren't objects on the heap :) You can reassign their value all you want, but they're always the same statically-allocated section of memory they started onFeathers
@Alexander-ReinstateMonica When doing that, do you only replace specific variable inside the struct or all of them?Delaney
@Delaney It depends on how clever the optimizer can get. If it knows that only 1 field could possibly have changed, itll only change the one field.Feathers
C
0

Maybe easier to understand by thinking in this way, even struct is mutated in place, since it's value type, the new value is different from the old value, so they are different instances by definition of value type.

Caption answered 23/3 at 2:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.