What is devirtualisation, and why does -O alone not remove a class method’s vtable lookup?
Predict first
With -O but no whole-module flag, what does the call use?
class C { func f() -> Int { 1 } }
@inline(never) func call(_ x: C) -> Int { x.f() }
print(call(C()))
- class_method
- a direct call
Predict first
And with -O -wmo?
class C { func f() -> Int { 1 } }
@inline(never) func call(_ x: C) -> Int { x.f() }
print(call(C()))
- class_method
- a direct call
Mental model. To skip the receptionist and dial the extension directly, you must be sure nobody has reassigned it. final is a signed promise, and whole-module knowledge is checking every desk yourself.
Devirtualisation replaces an indirect call (a vtable class_method or a witness-table lookup) with a direct one, which can then be inlined. To do it the compiler must prove which implementation will run. For a class method that means proving no subclass overrides it. A final class or method is a proof by declaration. Otherwise the compiler needs to see every class in the program and find that nothing overrides it.
That is why -O on its own often leaves class_method in place: a per-file build cannot rule out a subclass in another file. With whole-module optimisation (-wmo) the compiler sees all files, and for an internal, private or fileprivate class it can conclude there is none, and call the method directly. A public or open class cannot be proven closed, because other modules may subclass it. Mark things final when you mean it; it is the one proof that always works.
A per-file build cannot prove that no subclass in another file overrides f, so the vtable lookup stays.
With every file visible, the compiler sees that no class overrides f and calls it directly.
Common traps
- Assuming
-Odevirtualises everything it could. Without whole-module visibility it must stay conservative. - Leaving library classes
publicbut never subclassed. Other modules might, so the compiler cannot assume otherwise. Mark themfinal.
They might follow up with
- Does devirtualisation need
finalon each method?final classcovers all members;final funccovers one. - What about protocol witness calls? The equivalent is specialisation, which pins the concrete type.
Related questions
- 5.2.2 What does whole-module optimisation change, and what does it cost?
- 5.1.4 How do you read SIL to tell static dispatch from dynamic dispatch?
- 1.11.2 Why does final (and whole-module optimization) help performance?
What does whole-module optimisation change, and what does it cost? →