Dynamic Dispatch Optimization

Hi!

I'm curious about the dynamic dispatch optimizations. Seems that the documentation was written almost 6 years ago. I wonder if this optimization and gains are still valid within Swift 5.

Yes. These optimizations have been standard since that point in time. What are your specific questions?

I was curious about declaring classes as final still being an optimization.
I wasn't sure if something changed given that the documentation was written so long ago.

I wouldn't think of it as an optimization. It is a semantic expression of a property of your class hierarchy. As a side-effect one has reduced dynamic dispatch. But that is not the primary purpose.

The optimization itself I was speaking about was auto-dynamic dispatch for internal (when compiling with WMO) and private things (always)

So if using final and private semantics in your code reduces dynamic dispatch and increases static, wouldn't that be an optimization?

i think its implied that there is some potential for optimization, but imagine that you implemented a final class and used it only through protocol interface... well guess what, you just got dynamic dispatch back again

or a final class a: b well you still have a superclass to deal with, its unclear that making b final when you still have superclass means you get static dispatch... does anyone know?

then the final thing is, what do your benchmarks say? did making all your classes final (with no protocol interface) result in a measurable improvement?

That would interesting to know... Sadly I haven't run tests on this as of right now.

I ran some tests almost two years back on a small Swift 4 project and we did see some improvements.

i see... i think it really depends on the situation... in a fairly large ios app, taking off final for view controllers and other "ns" objects didint have any measurable effect when we benchmarked (data and other things are mostly structs for us) so i suspect if you are subclassing nsobject or using protocols already, final might not help much...

and as other posters might say, i think id agree that making something final should be done for system design reasons not performance since that is a "side effect" a.k.a implementation detail :person_shrugging:t2:

Final can still give static dispatch with a subclass as long as you aren't passing it around as an instance of the base class.

class Base {
    init() { }
    func foo() {
        print("Base")
    }
}

final class Sub: Base {
    override func foo() {
        print("Sub")
    }
}

struct Caller {
    var callback: () -> Void
    var object: Base
    init<T: Base>(_ object: T) {
        self.object = object
        self.callback = {
            object.foo()
        }
    }
    func doStuff() {
        print("Static dispatch to foo.")
        callback()
        print("Dynamic dispatch to foo.")
        object.foo()
    }
}

Caller(Sub()).doStuff()

The reason that you still have dynamic dispatch is because Cocoa is generally not operating directly on your subclass but instead calling methods on its superclass.

right, that makes sense

since swift has basically a static type system, if you use it as class sub it will be used as that class and apply the potential optimizations... if you cast it as base then it knows there is a subclass so will use it in that context (not devirtualized)... same as when going through a protocol... then, in the end it all depends on "how you use it"...

(hope i got that right ^^;)

1 Like

@Nobody1707 that's interesting.

Does the same apply to protocols?

That's a little complicated, but the short answer is yes. The big difference is that the compiler is much more likely to be able to devirtualize the call for generics over protocols than if you pass an existential .

protocol P {
    func foo() // protocol requirements are dynamically dispatched.
}

func bar<T: P>(_ x: P) {
    x.foo() // Dynamic dipaatch that will almost certainly be devirtualized.
}

func bar(_ x: P) {
    /* This is such an easy mistake to make that the compiler tries extra hard
     * to devirtualize it, but you really oughtn't be relying on it.
     */
    x.foo() 
}

Of course, if the compiler knows all the types involved there's always a chance it could devirtualize any given call, it just gets less and less likely to do so the more dynamic the code is. This applies to classes as well, but it's important to note that it's still doing the same dispatch, it just constant propagates the vtables and figures out what to call at compile time.

1 Like