What is exclusivity enforcement, and what does it cost at runtime?

5.2.4 · L5 · Learning

Predict first

Does this compile?

var x = 1
func f(_ a: inout Int, _ b: inout Int) {}
f(&x, &x)
  • Yes
  • No

Predict first

What happens at runtime?

final class Box { var n = 0 }
func run() {
    let b = Box()
    func bump(_ a: inout Int) { b.n += 1 }
    bump(&b.n)
}
run()
print("survived")
  • Prints survived
  • Crashes with a simultaneous-access error

Predict first

How is the class property access marked in SIL?

final class Box { var n = 0 }
func run() {
    let b = Box()
    func bump(_ a: inout Int) { b.n += 1 }
    bump(&b.n)
}
run()
print("survived")
  • begin_access [modify] [static]
  • begin_access [modify] [dynamic]
  • no access marker

Mental model. Two people are not allowed to edit the same cell at once. Where the editor can see both in advance it says no, and elsewhere a guard checks at the door.

Swift requires that a variable is not accessed in two overlapping ways when one of them is a modification: you cannot pass &x twice to a function with two inout parameters, nor modify a property while another access to it is in progress. This law of exclusivity is what lets the compiler assume an inout argument cannot alias anything else, and so optimise freely.

It is enforced two ways. Where the compiler can see both accesses, such as local variables and struct stored properties, it rejects the code statically (“inout arguments are not allowed to alias each other”). Where it cannot, such as class properties, globals and escaping closures, it inserts dynamic checks: begin_access [dynamic] in SIL calls into the runtime, which tracks active accesses and crashes with “Simultaneous accesses … but modification requires exclusive access”. The checks are cheap but not free, and they are why unusual hot loops over class properties sometimes show runtime calls. They can be disabled with -enforce-exclusivity=unchecked, at the price of undefined behaviour if you actually overlap.

Both arguments modify x at the same time. The compiler can see this statically and rejects it.

The compiler cannot see that bump touches the same property, so a dynamic check catches the overlap when it happens.

A dynamic marker makes the runtime record and check the access.

Common traps

  • Disabling the checks to gain speed. You keep the optimiser’s assumption and lose the safety net, so a real overlap becomes silent corruption.
  • Passing a class property inout to a method on the same object. It compiles, then crashes at runtime.

They might follow up with

  • Why are class properties checked dynamically? Any code holding the same reference could access them, so the compiler cannot see all accesses.
  • Do reads conflict? Two reads never do. A read overlapping a modification does.

Related questions

  • 5.1.2 What does the compiler guarantee through its mandatory SIL passes?
  • 1.8.4 What does defer guarantee? What is the order of multiple defers?

← How does the inliner decide, and what do @inline(never)…