Does a mutating struct function in swift create a new copy of self?

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.


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.


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.