Performance of class instances versus closure-captured variables

A while back I've developed a tactic for mutating variables inside non-mutating methods of struct types:

public struct Variable<Value> {

    public init(_ value: Value) {
        var storage = value
        self.getter = { storage }
        self.setter = { storage = $0 }
    }

    public var value: Value {
        get {
            return self.getter()
        }

        nonmutating set {
            self.setter(newValue)
        }
    }

    private let getter: () -> Value

    private let setter: (Value) -> Void

}

// As opposed to simply using a class:

public final class Variable<Value> {

    public init(_ value: Value) {
        self.value = value
    }

    public var value: Value

}

At first, it seemed like an obvious gain in performance compared to classes, because no vtable needs to be generated and heap allocation is only done on parts of the type that do need to be mutated this way, but then, considering that one can have final root classes, it seems like there's a good chance that the compiler might be able to optimize the use of such a class much more heavily, then this admittedly dirty trick.

So, I was wondering, could anyone more familiar with compiler internals than me shed some light on which of these approaches would generally be faster?

I believe that if the compiler can verify that a class is not ever subclassed, it can devirtualize methods. I'm not sure how granular it gets i.e able to devirtualize individual methods, but I wouldn't be surprised if it did.

Well, that's what I figuted. Considering that it's neither subclassing anything nor allows to be subclassed, granular devirtualization is not even necessary and the whole thing would be reduced to a glorified struct with a static method call. The question is: is my closure capture hack actually faster? I should probably run some benchmarks, but I though I'd get some insight first.

I would expect that the class is slightly faster now, but it's also way more optimizable, since you're not obscuring your intent from the compiler. If you've got something that's really performance-sensitive, though, don't take my word for it; I don't usually work on run-time performance.

Yeah, the optimizability potential for the class seems significant, whereas the closure capture case is very obscure and not easy to statically analyze. I'll run some benchmarks to see how they compare at the moment, but I'd fully expect the class case to get faster over the course of compiler evolution.

Feel free to add your benchmarks to the Swift benchmark suite eventually :-) It should not be too hard even if you're not familiar with the specifics of the setup.

1 Like

I'll start by researching proper scientifically sound benchmarking practices (not to mention getting acquainted with the Swift benchmark suite), since I haven't really done official benchmarks before. Thanks for the tip! :slightly_smiling_face:

If you have any questions about benchmarking, feel free to ask.