What reflection metadata does the compiler emit for every type, what depends on it, and what does -disable-reflection-metadata do?

5.8.4 · L5 · Learning

Predict first

What does this print with the default compiler settings?

struct Account { var id = 1; var owner = "x"; var balance = 2.5 }
print(Mirror(reflecting: Account()).children.map { $0.label ?? "?" })
print(String(describing: Account()))
  • ["id", "owner", "balance"] / Account(id: 1, owner: "x", balance: 2.5)
  • [] / Account()
  • ["_id", "_owner", "_balance"] / Account(id: 1, owner: "x", balance: 2.5)
  • a runtime error

Predict first

Same program compiled with -Xfrontend -disable-reflection-metadata?

struct Account { var id = 1; var owner = "x"; var balance = 2.5 }
print(Mirror(reflecting: Account()).children.map { $0.label ?? "?" })
print(String(describing: Account()))
  • [] / Account()
  • ["id", "owner", "balance"] / Account(id: 1, owner: "x", balance: 2.5)
  • a compile error
  • a crash in Mirror

Mental model. Reflection metadata is the labels inside a filing cabinet. The cabinet works without them, but anybody who wants to read “what is in drawer two” gets silence once you peel them off.

For each struct, class and enum the compiler emits field descriptors: tables of stored-property names and types, in sections such as __swift5_fieldmd and __swift5_reflstr. They exist so the runtime can walk a value generically. Mirror(reflecting:) reads them, so do String(describing:) and print for a struct (that is where Account(id: 1, …) comes from), dump, the Xcode memory graph and debugger summaries, and some libraries that serialise by reflection. Things that do not depend on them include synthesised Codable (the compiler generates real code) and normal property access.

The frontend flag -Xfrontend -disable-reflection-metadata drops those tables to shrink the binary. The run below shows what you give up: with it, Mirror returns no children and a struct prints as Account(). In this three-field toy the executable shrank by only a few hundred bytes (55,424 to 55,216), so the saving is proportional to how many types you have; I did not measure a large app. It is a flag for app code that you know never reflects, with tests that check your logging and debug descriptions still look right, and it is not something to enable inside a library that others will inspect. The related -Xfrontend -reflection-metadata-for-debugger-only exists for debuggers; I did not run it.

Mirror and the default description read the field descriptors.

The field tables are gone, so Mirror finds no children and the description has no fields.

Common traps

  • Enabling the flag and then wondering why logs lost their field values. print(struct) and dump both use these tables.
  • Expecting a big win from a toy measurement. It scales with type and property count; measure your own app’s binary size.

They might follow up with

  • Does this affect Codable or SwiftUI? Synthesised Codable generates code, so no; I did not run SwiftUI with the flag, so verify before shipping.
  • What else makes the binary bigger? Generic specialisation, inlining, and linked frameworks; see the size question linked below.

Related questions

  • 21.7.2 Where does binary size come from, how do you measure and shrink it, and what do optimisation level, stripping…
  • 5.3.1 How can you see that a generic function was specialised, and what does it depend on?
  • 5.1.3 How do you look at what the compiler produced at each stage?
  • 3.4.2 What do MemoryLayout’s size, stride and alignment mean, and why does field order matter?

← When is a Swift global or static property initialised, what is…