How do you read SIL to tell static dispatch from dynamic dispatch?
Predict first
What does the SIL call of a method on a final class contain?
final class F { func f() -> Int { 1 } }
@inline(never) func call(_ x: F) -> Int { x.f() }
print(call(F()))
- class_method
- function_ref
- witness_method
Predict first
What does it contain for an ordinary (non-final) class?
class C { func f() -> Int { 1 } }
@inline(never) func call(_ x: C) -> Int { x.f() }
print(call(C()))
- class_method
- function_ref
- witness_method
Predict first
What does it contain for a call through any P?
protocol P { func f() -> Int }
struct S: P { func f() -> Int { 1 } }
@inline(never) func call(_ x: any P) -> Int { x.f() }
print(call(S()))
- function_ref only
- open_existential_addr then witness_method
- class_method
Mental model. The instruction names how the callee is found: a street address (function_ref), a look-up in the company directory (class_method), or a call to the department that does it (witness_method).
Look at the instruction that precedes the apply (the call). function_ref names one specific function: a direct call, which can be inlined. class_method looks the function up in the class’s vtable at runtime. witness_method looks it up in a protocol witness table, the mechanism behind existentials and unspecialised generics. objc_method is an Objective-C message send. An existential call also shows open_existential_addr, which unwraps the box first.
That lets you verify a claim instead of repeating it. A final class method is a function_ref; an ordinary class method is a class_method; a call through any P is an open_existential_addr followed by a witness_method. The optimiser’s job, seen in SIL, is turning the second and third kinds into the first. See the devirtualisation and specialisation questions for when that works.
A final class cannot be overridden, so the compiler names the exact function: a direct function_ref.
A subclass could override f, so the implementation is looked up in the vtable at runtime.
The call unwraps the existential box and then finds the method in the protocol witness table.
Common traps
- Reading the Swift and guessing. The optimiser changes the answer between Debug and Release; check the SIL of the build you ship.
- Assuming
privatemeans direct. Without whole-module knowledge a call can still go through the vtable.
They might follow up with
- What is
thinversusmethodin the printed type? It is the calling convention. Athinfunction takes no context pointer. - Where is the vtable? A
sil_vtablesection lists the entriesclass_methodindexes into.
Related questions
- 5.1.3 How do you look at what the compiler produced at each stage?
- 5.2.1 What is devirtualisation, and why does -O alone not remove a class method’s vtable lookup?
- 1.11.1 Static vs dynamic dispatch in Swift?
← How do you look at what the compiler produced at each stage?