4
votes

The general consensus for Swift programming (as at May 2018, Swift 4.1, Xcode 9.3) is that structs should be preferred unless your logic explicitly calls for a shared reference to an object.

As we know, a problem with structs is that they're passed by-value, and so a copy is made when you pass a struct into, or return from a function. If you have a large struct (say with 12 properties in it) then this copying could get expensive.

This is usually defended by people saying that the swift compiler and/or LLVM can elide the copies (I.e. pass a reference to a struct, rather than copying it) and only needs to make a copy if you actually mutate the struct.

This is all well and good, but it's always talked about in theoretical terms - "As an optimisation, LLVM could elide the copies" and stuff like that.

My question is, can anyone tell us what actually happens? Does the compiler actually elide the copies, or is it just a theoretical future optimization that might exist one day? (For example, the C# compiler could also theoretically elide struct copies, but it never actually does this, and Microsoft recommends you don't use structs for things larger than 16 bytes [1])

If swift does elide struct copies, is there some explanation or heuristic as to if and when it does this?

Note: I'm talking about user-defined structs, not built in stdlib things like arrays and dictionaries

[1] https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/choosing-between-class-and-struct

1
As you're referring to a C++ term here, I'll point out that C++ copies have the potential to be orders of magnitude larger than Swift copies. The cost of copying a vector 10,000 non-trivial objects in C++ is going to dwarf the cost of copying a "large struct" of 12 properties. - zneak
The compiler can also specialise functions that take value type parameters such that only the properties that are used within the function body are passed, compare stackoverflow.com/q/43486408/2976878 - Hamish

1 Answers

5
votes

First, Swift does not use the platform's calling convention. On macOS, C, C++ and Objective-C all use the x86_64 System V ABI, but Swift doesn't. A notable change is that Swift's CC has four return GPRs (rax, rdx, rcx, r8) instead of just two.

It almost certainly gets more complicated when you mix in floating-point numbers, but if you go all integer and integer-like types (like pointers), structures are passed and returned by register, by copy, if they fit in the width of at most 4 registers. Above that, structures are passed and returned by address. In the case of a return value, the caller is responsible for setting up stack space and passing the address of that space to the callee as a hidden parameter.

As the Swift ABI isn't finalized, this is still subject to change, possibly.

However, merely passing pointers doesn't mean that no copies happen. For instance:

public class Let {
    let large: Large

    init(large: Large) {
        self.large = large
    }
}

public func withLet(l: Let) {
    doSomething(foo: l.large)
}

In this example, at -O on Swift 4.1, withLet makes the following tradeoff:

  • l.large is copied to a local temporary
  • l is released after the copy and before doSomething is called

A copy would be unavoidable with a mutable or computed property (because their value can change across the duration of a call), but I imagine that it's in the realm of possibilities that let constants could be passed by address directly. However, in that case, l would have to stay alive until after doSomething has returned.