swiftperformancestructswift-structs

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


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)

Solution

  • 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.