didSet is called on property mutation

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
8 messages Options
Reply | Threaded
Open this post in threaded view
|

didSet is called on property mutation

Jens Alfke
I’ve run into something unexpected: if I add a didSet block to the definition of a struct property, it’s called when a mutating method is called on the current property value, not just when a new value is assigned. In other words, “c.foo.doSomething()”, where doSomething() is a mutating method, will invoke a didSet block for the property c.foo. There’s no mention of this in the Swift book, as far as I can tell.

Here’s an example:

import Foundation

struct Foo : Printable {
    private var x: Int = 0
    mutating func add(n: Int) {
        x += n
    }
    var description: String {
        return "Foo(\(x))"
    }
}

struct Container {
   var foo = Foo() {
        didSet {
            println("didSet foo! was \(oldValue) now \(foo)")
        }
    }
}

var c = Container()
println(c.foo)
c.foo.add(10)
println(c.foo)

The output when run is:
Foo(0)
didSet foo! was Foo(0) now Foo(10)
Foo(10)

The weirdest part is that the didSet block has access to the struct in both its old and new state. In other words, it looks like the struct is copied before the mutating method is called, so that the didSet block can be passed the copy as its ‘oldValue’ parameter. Now I’m curious whether this behavior happens only if a didSet (or willSet) block is present, or if structs are always mutated this way. It could get expensive to make copies all the time, especially if the struct has references to class objects whose refcounts need to be adjusted.

—Jens

--
You received this message because you are subscribed to the Google Groups "Swift Language" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [hidden email].
To post to this group, send email to [hidden email].
To view this discussion on the web visit https://groups.google.com/d/msgid/swift-language/02D3D5F8-8A61-48BA-B145-2F0B748E99B5%40mooseyard.com.
For more options, visit https://groups.google.com/d/optout.
Reply | Threaded
Open this post in threaded view
|

Re: didSet is called on property mutation

Chris Lattner

On Jun 2, 2015, at 3:11 PM, Jens Alfke <[hidden email]> wrote:

I’ve run into something unexpected: if I add a didSet block to the definition of a struct property, it’s called when a mutating method is called on the current property value, not just when a new value is assigned. In other words, “c.foo.doSomething()”, where doSomething() is a mutating method, will invoke a didSet block for the property c.foo. There’s no mention of this in the Swift book, as far as I can tell.

Hi Jens,

This is an intentional, and very important part of the model.  I explore this area in a bit more detail here:

-Chris


Here’s an example:

import Foundation

struct Foo : Printable {
    private var x: Int = 0
    mutating func add(n: Int) {
        x += n
    }
    var description: String {
        return "Foo(\(x))"
    }
}

struct Container {
   var foo = Foo() {
        didSet {
            println("didSet foo! was \(oldValue) now \(foo)")
        }
    }
}

var c = Container()
println(c.foo)
c.foo.add(10)
println(c.foo)

The output when run is:
Foo(0)
didSet foo! was Foo(0) now Foo(10)
Foo(10)

The weirdest part is that the didSet block has access to the struct in both its old and new state. In other words, it looks like the struct is copied before the mutating method is called, so that the didSet block can be passed the copy as its ‘oldValue’ parameter. Now I’m curious whether this behavior happens only if a didSet (or willSet) block is present, or if structs are always mutated this way. It could get expensive to make copies all the time, especially if the struct has references to class objects whose refcounts need to be adjusted.

—Jens

--
You received this message because you are subscribed to the Google Groups "Swift Language" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [hidden email].
To post to this group, send email to [hidden email].
To view this discussion on the web visit https://groups.google.com/d/msgid/swift-language/02D3D5F8-8A61-48BA-B145-2F0B748E99B5%40mooseyard.com.
For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "Swift Language" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [hidden email].
To post to this group, send email to [hidden email].
To view this discussion on the web visit https://groups.google.com/d/msgid/swift-language/D297A3F2-4AB3-4D79-866E-114492AAD085%40apple.com.
For more options, visit https://groups.google.com/d/optout.
Reply | Threaded
Open this post in threaded view
|

Re: didSet is called on property mutation

Jens Alfke

On Jun 2, 2015, at 5:05 PM, Chris Lattner <[hidden email]> wrote:

This is an intentional, and very important part of the model.  I explore this area in a bit more detail here:

That’s very interesting, and definitely deserves calling out in the book — the actual semantics of ‘inout’ and the unary ‘&’ operator seem different than what most people would expect.

As an inveterate cycle-counter, I’m concerned about the cost of implementing mutating methods this way. Copying the entire struct (twice) is more expensive than operating on it in place, particularly as the size of the struct grows. Does the copying always occur, or are there situations where it can be optimized away?

—Jens

--
You received this message because you are subscribed to the Google Groups "Swift Language" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [hidden email].
To post to this group, send email to [hidden email].
To view this discussion on the web visit https://groups.google.com/d/msgid/swift-language/7B3FE6FB-AE18-4612-BF48-767274F03F6A%40mooseyard.com.
For more options, visit https://groups.google.com/d/optout.
Reply | Threaded
Open this post in threaded view
|

Re: didSet is called on property mutation

Chris Lattner

On Jun 3, 2015, at 2:49 PM, Jens Alfke <[hidden email]> wrote:


On Jun 2, 2015, at 5:05 PM, Chris Lattner <[hidden email]> wrote:

This is an intentional, and very important part of the model.  I explore this area in a bit more detail here:

That’s very interesting, and definitely deserves calling out in the book — the actual semantics of ‘inout’ and the unary ‘&’ operator seem different than what most people would expect.

As an inveterate cycle-counter, I’m concerned about the cost of implementing mutating methods this way. Copying the entire struct (twice) is more expensive than operating on it in place, particularly as the size of the struct grows. Does the copying always occur, or are there situations where it can be optimized away?

The inout copy in the callee is almost always eliminated.  The only place is remains is when captured by a (non-@noescape) closure that isn’t otherwise optimized away (e.g. by being inlined).

-Chris

--
You received this message because you are subscribed to the Google Groups "Swift Language" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [hidden email].
To post to this group, send email to [hidden email].
To view this discussion on the web visit https://groups.google.com/d/msgid/swift-language/5E12F3B6-1D06-496F-BE70-151BD553A11B%40apple.com.
For more options, visit https://groups.google.com/d/optout.
Reply | Threaded
Open this post in threaded view
|

Re: didSet is called on property mutation

Chris Lattner

On Jun 3, 2015, at 3:53 PM, Chris Lattner <[hidden email]> wrote:


On Jun 3, 2015, at 2:49 PM, Jens Alfke <[hidden email]> wrote:


On Jun 2, 2015, at 5:05 PM, Chris Lattner <[hidden email]> wrote:

This is an intentional, and very important part of the model.  I explore this area in a bit more detail here:

That’s very interesting, and definitely deserves calling out in the book — the actual semantics of ‘inout’ and the unary ‘&’ operator seem different than what most people would expect.

As an inveterate cycle-counter, I’m concerned about the cost of implementing mutating methods this way. Copying the entire struct (twice) is more expensive than operating on it in place, particularly as the size of the struct grows. Does the copying always occur, or are there situations where it can be optimized away?

The inout copy in the callee is almost always eliminated.  The only place is remains is when captured by a (non-@noescape) closure that isn’t otherwise optimized away (e.g. by being inlined).

One additional thing: The semantic model here is very similar to local variables.  Semantically (and worst case) all local variables are heap allocated:

  func foo() {
     var x = 42 // semantically this is on the heap
  }

however, this is only visible when closed over by an escaping closure.  In the vastly most common cases, these are actually put on the stack according to the standard “as if” rule that compilers use.

Also, FWIW, these things are even done at -O0.

-Chris

--
You received this message because you are subscribed to the Google Groups "Swift Language" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [hidden email].
To post to this group, send email to [hidden email].
To view this discussion on the web visit https://groups.google.com/d/msgid/swift-language/873D0403-33DC-46D3-AAAB-67FE7165E0B7%40apple.com.
For more options, visit https://groups.google.com/d/optout.
Reply | Threaded
Open this post in threaded view
|

Re: didSet is called on property mutation

Jens Alfke
In reply to this post by Chris Lattner

On Jun 3, 2015, at 3:53 PM, Chris Lattner <[hidden email]> wrote:

The inout copy in the callee is almost always eliminated.  The only place is remains is when captured by a (non-@noescape) closure that isn’t otherwise optimized away (e.g. by being inlined).

…or when the receiver of the mutating method is a property of another object, right? Since that’s the case I ran into. There’s definitely copying going on in that case, because my didSet block gets passed both the old and the new value of the struct.

—Jens

--
You received this message because you are subscribed to the Google Groups "Swift Language" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [hidden email].
To post to this group, send email to [hidden email].
To view this discussion on the web visit https://groups.google.com/d/msgid/swift-language/A29441FC-479E-49F8-9039-94360B14BEF0%40mooseyard.com.
For more options, visit https://groups.google.com/d/optout.
Reply | Threaded
Open this post in threaded view
|

Re: didSet is called on property mutation

Chris Lattner

On Jun 3, 2015, at 4:41 PM, Jens Alfke <[hidden email]> wrote:


On Jun 3, 2015, at 3:53 PM, Chris Lattner <[hidden email]> wrote:

The inout copy in the callee is almost always eliminated.  The only place is remains is when captured by a (non-@noescape) closure that isn’t otherwise optimized away (e.g. by being inlined).

…or when the receiver of the mutating method is a property of another object, right? Since that’s the case I ran into. There’s definitely copying going on in that case, because my didSet block gets passed both the old and the new value of the struct.

That’s a different case, and you’re right that that is not always eliminated (please file a radar with a small testcase if you can though).

The case I was referring to is:

func f<T>(inout a : T) {
   do_closure_thing { a }
}

In this case, a copy has to be made callee side of f, because the closure could extend the lifetime of the thing passed in.  It is surprising to many folks, but the name “inout” was chosen because the semantics of the code above are:

func f<T>(inout tmp : T) {
   var a = tmp    // "in"
   do_closure_thing { a = whatever() }
   tmp = a     // "out"
}

which can surprise people because mutations to the closed over local variable only get seen in the caller if they happen before “f" returns.

-Chris


--
You received this message because you are subscribed to the Google Groups "Swift Language" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [hidden email].
To post to this group, send email to [hidden email].
To view this discussion on the web visit https://groups.google.com/d/msgid/swift-language/A5CEF2AC-80DC-448B-A8DA-0E5F136F57CF%40apple.com.
For more options, visit https://groups.google.com/d/optout.
Reply | Threaded
Open this post in threaded view
|

Re: didSet is called on property mutation

Matthias Zenger
Chris, I'm confused now. I just started to read the Swift language manual and found the section on "inout" parameters totally under-specified. So, I did a little bit of experimentation. The first code I wrote was along the lines of this:

var global = 0


func foo(inout x: Int) -> (Int, Int, Int) {

 func inc() -> Int {

   return ++x

 }

 return (global, inc(), global)

}


foo(&global)    // (0, 1, 1)
global          // 1


The result of foo(&global) is (0, 1, 1), which indicates that this is, in fact, call by reference, because ++x mutates both x and global simultaneously. Motivated by your post, I inserted an assignment into foo which stores inc in global variable f:


var global = 0

var f = { 0 }


func foo(inout x: Int) -> (Int, Int, Int) {

 func inc() -> Int {

   return ++x

 }

 f = inc

 return (global, inc(), global)

}


foo(&global)    // (0, 1, 0)
global          // 1


It was a total surprise for me to see that just by adding an innocent assignment, which shouldn't impact the computation at all, the result of the function call foo(&global) changed to (0, 1, 0). So, here, the "inout" parameter has real "inout semantics" in that it assigns the result of ++x to global only when the function returns to the client.


Am I right in concluding that the Swift compiler currently incorrectly optimizes cases where the inout parameter isn't captured by a closure that escapes the scope by simply using call by reference?


== Matthias



On Thursday, June 4, 2015 at 1:51:36 AM UTC+2, Chris Lattner wrote:

On Jun 3, 2015, at 4:41 PM, Jens Alfke <<a href="javascript:" target="_blank" gdf-obfuscated-mailto="8xXNWjpwylMJ" rel="nofollow" onmousedown="this.href='javascript:';return true;" onclick="this.href='javascript:';return true;">je...@...> wrote:


On Jun 3, 2015, at 3:53 PM, Chris Lattner <<a href="javascript:" target="_blank" gdf-obfuscated-mailto="8xXNWjpwylMJ" rel="nofollow" onmousedown="this.href='javascript:';return true;" onclick="this.href='javascript:';return true;">clat...@...> wrote:

The inout copy in the callee is almost always eliminated.  The only place is remains is when captured by a (non-@noescape) closure that isn’t otherwise optimized away (e.g. by being inlined).

…or when the receiver of the mutating method is a property of another object, right? Since that’s the case I ran into. There’s definitely copying going on in that case, because my didSet block gets passed both the old and the new value of the struct.

That’s a different case, and you’re right that that is not always eliminated (please file a radar with a small testcase if you can though).

The case I was referring to is:

func f<T>(inout a : T) {
   do_closure_thing { a }
}

In this case, a copy has to be made callee side of f, because the closure could extend the lifetime of the thing passed in.  It is surprising to many folks, but the name “inout” was chosen because the semantics of the code above are:

func f<T>(inout tmp : T) {
   var a = tmp    // "in"
   do_closure_thing { a = whatever() }
   tmp = a     // "out"
}

which can surprise people because mutations to the closed over local variable only get seen in the caller if they happen before “f" returns.

-Chris


--
You received this message because you are subscribed to the Google Groups "Swift Language" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [hidden email].
To post to this group, send email to [hidden email].
To view this discussion on the web visit https://groups.google.com/d/msgid/swift-language/0d71fb13-2621-47f2-b3a1-afe58e78dc3c%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.