What is different in the compiled code between returning some P and any P?

5.3.3 · L5 · Interview

Predict first

Which constructs appear in the SIL for the some P version?

protocol P { func f() -> Int }
struct S: P { func f() -> Int { 1 } }
func make() -> some P { S() }
@inline(never) func use() -> Int { make().f() }
_ = use()
  • init_existential_addr and witness_method
  • neither, the call is direct

Predict first

And for the any P version?

protocol P { func f() -> Int }
struct S: P { func f() -> Int { 1 } }
func make() -> any P { S() }
@inline(never) func use() -> Int { make().f() }
_ = use()
  • init_existential_addr and witness_method
  • neither, the call is direct

Mental model. some is a sealed box you know the contents of but may not show anyone. any is a box whose contents even you must open and inspect each time.

Both hide a concrete type from the caller’s source, but the compiler treats them very differently. With some P the function’s return type is one specific type chosen by the body. The compiler keeps knowing it, so the caller’s call to .f() becomes a direct call (a function_ref), and the result can live unboxed on the stack or in registers. With any P the value is packed into an existential container (init_existential_addr), and the caller must unwrap it (open_existential_addr) and look the method up in a witness table (witness_method) every time.

That is the whole performance story behind the choice, now visible in the output: some keeps static knowledge, any discards it. The cost of any is real in a hot loop and irrelevant at an API boundary. Reach for any when you genuinely need to store different concrete types together, and for some or a generic when you do not.

The concrete type is known, so there is no existential container and no witness lookup.

The value is boxed on the way out, and the call unwraps it and finds the method through a witness table.

Common traps

  • Using any for a single concrete type “for flexibility”. You pay for boxing and dynamic calls with nothing gained.
  • Assuming some allows different types per branch. It is still one type; the compiler needs to know which.

They might follow up with

  • When does an existential avoid heap allocation? When the value fits the three-word inline buffer; larger values are boxed.
  • Can the optimiser remove an any? Sometimes, when the concrete type is visible after inlining. Do not count on it.

Related questions

  • 5.3.2 How does an unspecialised generic function know the concrete type, and what does that cost?
  • 5.1.4 How do you read SIL to tell static dispatch from dynamic dispatch?
  • 1.4.2 some View vs any View — what is the difference, and why does SwiftUI use some?

← How does an unspecialised generic function know the concrete type…