What copy-elision does swift actually do for structs?

284 Views Asked by At

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://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/choosing-between-class-and-struct

1

There are 1 best solutions below

3
On

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.