4

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

Orion Edwards
  • 121,657
  • 64
  • 239
  • 328
  • 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 May 16 '18 at 02:55
  • 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 https://stackoverflow.com/q/43486408/2976878 – Hamish May 16 '18 at 10:18

1 Answers1

5

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.

zneak
  • 134,922
  • 42
  • 253
  • 328
  • Your answer implies that for immutable structs larger than 4 registers they are *not* copied when passed to a function, but doesn't explicitly say this - am I reading it right? – Orion Edwards May 17 '18 at 00:57
  • @OrionEdwards, I didn't say that because it is heavily modulated by the second half of the answer. In practice, although there's evidence that it tries to minimize them, it's hard to predict whether the compiler will or won't make a copy. In [this example](https://pastebin.com/raw/RWfUC9UJ), with Swift 4.1, `large` is constructed once and passed by address directly to `foo`, which doesn't copy it. However, in [this other similar example](https://pastebin.com/raw/qNN8s30b), the compiler creates a *new* copy of `large`, just to change the first field, before passing it to the second `foo` call. – zneak May 17 '18 at 01:37
  • (I should clarify that the second copy is created from a constant `(5, 6, 5, 4, 3, 2, 1, 0)` tuple, not from the other structure. This leads me to believe that it's a consequence of the SSA form that LLVM uses, but it still qualifies as a copy to me.) – zneak May 17 '18 at 01:47