Proposal for indirect struct properties

(I apologize that this post is long. The original version was short and linked to a longer post on my blog, but that version was stuck in moderation for days because of the external link. This time I've reproduced the entire post here.)


I’m continuing to listen to the WWDC videos and think about how they apply to my own code, and recently I’ve been thinking about the intersection of three Swift topics:


  • Building Better Apps with Value Types in Swift, where they promote the advantages of value types over reference types for certain uses.
  • Optimizing Swift Performance, where they describe possible performance issues with value types and how to work around them.
  • What’s New in Swift, where they describe the (currently unimplemented) indirect keyword for use in recursive enums.


In the optimization video, Michael Gottesman points out that Swift’s copy-on-write semantics for structs can cause performance problems if the struct contains many reference-counted properties. (14:12) The first time a struct is modified it must be copied, which means that the reference counts of all reference properties must be incremented. His solution is to group the reference properties into a wrapper class and store an instance of the wrapper class in the struct. That way only one reference count needs to be incremented when the struct is copied. In the value types talk, Bill Dudney shows how to implement this workaround efficiently for Swift types using the isUniquelyReferencedNonObjC function. (32:25)


This solves the performance problem, but at a cost in legibility and maintenance. For example, take this simple struct with three reference properties:


struct Customer {
    var firstName: String
    var middleName: String
    var lastName: String
}


If you apply the wrapper class optimization to this struct, you get something like this:


struct Customer {
    private class Wrapper {
        var firstName = ""
        var middleName = ""
        var lastName = ""
      
        func copy() -> Wrapper {
            let clone = Wrapper()
            clone.firstName = firstName
            clone.middleName = middleName
            clone.lastName = lastName
            return clone
        }
    }
  
    private var _values = Wrapper()
  
    private var pathForReading: Wrapper {
        get {
            return _values
        }
    }
  
    private var pathForWriting: Wrapper {
        mutating get {
            if !isUniquelyReferencedNonObjC(&_values) {
                _values = _values.copy()
            }
            return _values
        }
    }
  
    var firstName: String {
        get {
            return pathForReading.firstName
        }
        set {
            pathForWriting.firstName = newValue
        }
    }
  
    var middleName: String {
        get {
            return pathForReading.middleName
        }
        set {
            pathForWriting.middleName = newValue
        }
    }
  
    var lastName: String {
        get {
            return pathForReading.lastName
        }
        set {
            pathForWriting.lastName = newValue
        }
    }
}


This is a lot of code to write and maintain for a simple (and presumably common) optimization. And although Swift can catch some common errors in this code, there are many that it can’t catch. For instance, while the compiler will complain if you use pathForWriting in a get method, it won’t say a word if you use pathForReading in a set method, which is arguably a much bigger problem. And of course it will have no idea if you make a copy/paste error and return pathForReading.firstName from the accessor for middleName.


A Proposed Improvement


If these wrapper classes are going to be common in production Swift code, then perhaps it would be better for Swift to provide built-in support for them. I was trying to figure out what keyword might be used to request this optimization when I remembered Chris Lattner’s talk where he reveals the (currently unimplemented) indirect keyword for creating recursive cases in enums.


To me, this seems like the right keyword for requesting indirect properties in structs as well.


From a certain point of view, enums and structs in Swift aren’t that different from one another. They are both value types that can contain sub-values. (The sub-values are known as associated values for enums, and properties for structs.) The main difference is that while an enum contains one of the sub-values, a struct contains all of the sub-values.


The indirect keyword being added to enums in Swift 2.0 solves a practical, rather than a conceptual, problem. Conceptually, there is no issue with an enum containing itself as an associated value, since a program can only create a finite number of enums and eventually the recursion must end. But in practice, associated values are stored directly in the enum and so indirection is needed to avoid allocating an infinitely large object.


The use of the indirect keyword that I’m proposing for structs is also to solve a practical problem — in this case, a performance problem. But the effect is essentially the same as with enums: the sub-values are moved out to a separate object and referenced indirectly.


If the indirect keyword were allowed in structs as well as enums, then the above code could simply become:


struct Customer {
    indirect var firstName = ""
    indirect var middleName = ""
    indirect var lastName = ""
}


The Swift compiler could gather all of the indirect properties into a wrapper class and take care of creating the hidden wrapper class, the hidden value property, and rewriting the getters and setters to indirect through the hidden property.


I’d love to hear what others think of this proposal.

A couple of points to consider:


— Type String is a value type, not a reference type. This doesn't affect your argument, but it makes it less urgent (because you need to give a plausible example that actually uses reference types).


— If the reference type is immutable, then there's an argument that says it should be a value type instead, in which case the need for this optimization goes away.


— If the reference type is mutable, then the value type with references is already an undesirable (and potentially dangerous) construct. It might be better to use a reference type at the top level to begin with.


— This only applies when there is actually a performance problem. Incrementing reference counts is actually quite fast, relative to much that happens in an ARC app.

It is clear in WWDC sessons that String as well as Array has a reference type for underlying storage. It only appears to be value type by encapsulating the common storage type shared with ObjC (something like CFString for toll-free bridging I assume).


"indirect" as the keyword though I think is confusing. It solves a completely different problem in enum, and it can't express more than one group.

What do you have in mind by "more than one group"? Is there a benefit to grouping different reference properties into different wrappers? I don't think that was discussed in the videos.

Some wires are getting crossed here, so it might help to clarify things for people who haven't watched the video recently.


Part of your description has some issues.

In the optimization video, Michael Gottesman points out that Swift’s copy-on-write semantics for structs can cause performance problems if the struct contains many reference-counted properties. (14:12) The first time a struct is modified it must be copied, which means that the reference counts of all reference properties must be incremented.


The problem that Michael Gottesman is showing how to optimize isn't really about copy-on-write, and is something that happens every time a value-type instance like a struct instance is assigned or passed (not just when the struct instance is modified).


Whenever a value-type instance is assigned to a variable or is passed to/from a function, the value-type instance is copied and the variable or parameter has an entirely new copy of that value-type instance. For the purposes of this discussion, you should assume that this kind of copying always happens (even though the compiler could hypothetically optimize a passed value as a passed reference in some cases, that's the compiler's business).


When the compiler makes the copy of the value-type instance, assuming it is a "pure" value-type (none of it's properties are reference-types, or value-types which have properties which are reference-types, etc) all it has to do is make a copy of the raw memory of the value-type. All of the "pure" value-type instance's properties (and the properties of those properties, if any, etc) are contained in that raw memory, so ARC (Automatic Reference Counting) doesn't have to worry about anything else.


If the value-type has a property which is a reference-type (or if it has properties which are other value-types which have reference-type properties), in order for ARC to keep proper track of those reference-type properties or sub-properties, it needs to increment the reference count of those reference-type properties in addition to making the raw memory copy of the value-type instance (because there is now an additional reference to the reference-type property, in the new copy of the value-type instance).


If your value-type has multiple properties which are reference-types (or value-types which have reference-type properties), performance-wise you will end up with more time spent dealing with incrementing / decrementing multiple reference counts, compared to a regular reference-type instance which only has to have its own reference count incremented / decremented.


That is the extra reference-type property incrementation occuring when copying a value-type instance, which Michael Gottesman was speaking about.



So, ideally, if you can make a value-type a "pure" value-type, it will entirely avoid the overhead from incrementing reference counts when being assigned or passed.


In the real world though, there are plenty of things that we would like to represent as a value-type, but can't reasonably be implemented as a "pure" value-type. If the type holds data with an arbitrary size, that isn't possible to represent efficeintly in a fixed-size "pure" value-type. If the associated data is large, you don't want to copy it all each time you assign or pass an instance around. Having a reference-type property inside the value-type is the solution to this, so that the arbitrary or large data isn't copied each time the instance of the value-type is assigned or passed, and only the reference is.


Copy-on-Write is how the "hybrid" value-type with a reference-type property keeps it's promise to act like a value-type. The copying that happens when a "hybrid" value-type is assigned or passed will copy the raw memory of the instance and increment the reference count of the reference-type property, but any changes to the reference-type property would still change all existing copies that point to the same reference-type instance.


The code which Bill Dudney went through in the Better Apps with Value Types session is basically how to implement that copy-on-write promise.



Getting back to the optimization of "hybrid" value-type assignment/passing which Michael Gottesman was talking about in the Optimization session:


In terms of performance, once a value-type instance is a "hybrid" with one or more reference-type properties, the best it can do when being assigned/passed is match the reference counting cost of a regular reference-type instance.


Since the cost of reference counting goes up when the "hybrid" value-type has more reference-type properties and sub-properties, it can be assigned/passed most efficiently when all of those reference-type properties are wrapped inside a single reference-type wrapper class inside the "hybrid" value-type, since the reference-type wrapper class is responsible for managing those other properties and ARC doesn't need to consider them during the copy.


If any of those wrapped properties are mutable, then the wrapper needs to be managed with the copy-on-write behavior shown by Bill Dudney.


That improvement in performance when being assigned/passed comes at some performance cost each time when one of the properties is accessed, so profiling your actual code is the best way to know if it would be worth it.

Proposal for indirect struct properties
 
 
Q