Developing intuition for when values will be copied

TL;DR: I'm looking for rules of thumb about the circumstances when optimized Swift code will copy a (nontrivial) struct/enum value.

Coming from C++ as I do, I'm used to thinking about the overhead of passing parameters by value vs by reference, or of copying a value vs using a move constructor. In Swift these things tend to be implementation details of the optimizer, but part of my brain still wonders whether I'm doing things efficiently.

I'd like to know if there are rules of thumb for what will cause a struct/enum value to be copied, so I can design appropriately.

Here are some things that I believe to be true:

  1. Class and actor instances are of course never copied. You have to explicitly create a new instance. (Same goes for ~Copyable values, for different reasons.)
  2. Tiny types like Int and Double are passed by value, usually in a register.
  3. Small-ish types that fit in "a few" registers, like structs/enums/tuples of a few primitive fields, are passed by value in multiple registers. (I know this is ISA-specific but I think it's true in both x86-64 and ARM.)
  4. Bigger types are passed by reference. This is safe because Swift guarantees that a mutable reference never coexists with read-only references. An inout argument is modified in place by the callee, without copying.
  5. Similarly, self is passed by reference to a method, and a mutating method modifies its receiver in place.
  6. I'm less sure about return values. In a C/C++ ABI small return values are usually returned in one or perhaps "a few" registers, and larger return values have space reserved by the caller, which passes a reference to that space as a hidden parameter. Not sure if Swift does that too. That can result in a copy if the struct exists as a stored value somewhere else and gets returned to the caller. (Unless the compiler is able to inline the function, maybe.)
  7. Explicitly assigning a value to another variable copies it (unless the compiler can optimize it away.)
  8. Properties with custom setters, and subscripts with setters, seem like the major place where implicit copies happen. If I write foo.string += "x", and Foo.string is a property with a custom getter/setter, Swift will call the getter and copy the String, then mutate the string, then call the setter to copy it back. Same thing with array[6].mutatingMethod() -- it calls the Array subscript to copy the value, invokes mutatingMethod(), then copies the result back.

Is this accurate? Are there more situations to consider?

This document on the calling convention may contain relevant information: swift/docs/ABI/CallingConvention.rst at main · swiftlang/swift · GitHub (and maybe a couple of the other documents in that ABI folder as well)

Without having re-read that recently, I'm pretty sure small values are returned across a couple registers, and larger structs as well as values of unknown size (e.g. f<T>() -> T) are returned indirectly as you describe for larger structs in C.

Some things, such as array index accessors, allow in-place modification. In the past this was achieved by providing a third accessor alongside get and set called _modify, but my understanding is that's being replaced by more well-defined features like borrowing accessors.

i find that thinking about procedure calling convention in Swift tends to obscure more than it illuminates. optimized Swift code often looks nothing like the stack diagrams you see in Intro To Systems Programming, what happens in practice is your entire call tree will get flattened into a few giant, heavily inlined slabs with values moving opaquely and fluidly between memory and registers. and this is usually what you want (ease of profiling notwithstanding), as these are the conditions under which Swift code executes most efficiently.

what you want to be thinking about instead is resilience boundaries (“where are the chocolate chips in my inlined brownie?”), whether you’ve configured the right reference counting conventions (borrowing/__shared versus consuming/__owned) along those boundaries, and whether you are mutating (allocated) things that are uniquely referenced.

It's still the case: swift/stdlib/public/core/Array.swift at 1b00845e5ae3f5600e1e5d87c71e67c5427fbd9f · swiftlang/swift · GitHub

Oh right, this is the 'coroutine-based' accessor that's going to become yielding mutate once SE-0474 is finished. I forgot to mention that.