What do @guaranteed and @owned mean in SIL, which does Swift use by default for parameters and initialisers, and how does that explain retain and release traffic?

5.9.3 · L5 · Learning

Predict first

What conventions does SILGen show for plain, taking and Holder.init?

import Swift
final class Box { var v = 0 }
@inline(never) func plain(_ b: Box) -> Int { b.v }
@inline(never) func taking(_ b: consuming Box) -> Int { b.v }
final class Holder { var b: Box; init(_ b: Box) { self.b = b } }
  • plain: @guaranteed Box; taking: @owned Box; init: @owned Box
  • all three @guaranteed
  • all three @owned
  • plain: @owned; taking: @guaranteed

Mental model. @guaranteed is lending a book that must be back on the shelf when the call ends. @owned is giving the book away: the receiver now has to shelve it or throw it out.

Every parameter in SIL has an ownership convention. @guaranteed means the caller keeps ownership and guarantees the value stays alive for the whole call (a “+0” argument): the callee may use it but must retain it itself if it wants to keep it. @owned means ownership is transferred to the callee (a “+1” argument), and the callee must consume or release it. The defaults are chosen for the common case: ordinary function and method parameters, and self, are @guaranteed, so passing a class reference to a function does not by itself retain it; initialiser arguments are @owned, because an initialiser usually stores them; and consuming changes a parameter to @owned, while borrowing is the explicit form of @guaranteed. In my output plain(_:) took @guaranteed Box, taking(_:) with consuming took @owned Box, and Holder.init took @owned Box.

That is why retain and release calls cluster where ownership changes: storing into a property, returning a value, or passing into an initialiser or consuming parameter, while a read-only call can avoid them. The optimiser also removes balanced pairs (an OSSA-based pass), so a count of swift_retain in -Onone says little about release performance. Use the conventions as a reading tool, not a promise: they are an implementation detail except where you spell out borrowing and consuming, which are the language-level ones.

Defaults are guaranteed for arguments, owned for initialiser arguments and consuming.

Common traps

  • Counting retains in debug SIL and drawing performance conclusions. Look at -O output.
  • Adding consuming to “speed things up” on a copyable type. It can force the caller to copy; use it where you really hand ownership over.

They might follow up with

  • What does inout look like? An @inout address convention: exclusive access, no ownership transfer.
  • Where do noncopyable types fit? Their conventions are enforced, see the consume and borrowing question.

Related questions

  • 1.8.5 What are consume, borrowing, and noncopyable types, at a high level?
  • 5.1.4 How do you read SIL to tell static dispatch from dynamic dispatch?
  • 3.2.1 What is the difference between strong, weak, unowned and unowned(unsafe)?
  • 5.2.4 What is exclusivity enforcement, and what does it cost at runtime?

← Where does a captured variable live: on the stack or in a heap box…   How big are Optional, enums and existentials in memory, and why is… →