Re: "What is ~Copyable for?" – Questions about superfluous copies

in reading through this discussion about the existential rationale for ~Copyable, these assertions in particular stood out to me:

this got me wondering – are the scenarios in which superfluous copies are produced fully characterized? does anyone have a concrete example of one? generally wondering what are the causes of this and what sorts of implementation changes would be needed to address it.

3 Likes

A recent example I looked at is the implementation of == on Optionals, defined here:

  public static func ==(lhs: Wrapped?, rhs: Wrapped?) -> Bool {
    switch (lhs, rhs) {
    case let (l?, r?):
      return l == r
    case (nil, nil):
      return true
    default:
      return false
    }
  }

We copy both input arguments to form the tuple value, and this tuple value is immediately consumed by the switch.

6 Likes

Without an explicit consume, wouldn’t copy elision require flow analysis to determine that lhs and rhs can be moved into the tuple?

Nothing needs to be consumed here. The arguments are borrowed, and the inner call to == also receives its arguments borrowed. If you write the equivalent definition using nested switches instead of switching over a tuple, there are no copies.

8 Likes

All the usual ways of unwrapping an Optional value that conforms to Copyable end up causing a copy:

struct MyStruct {
    var value: Int
    var existential: Any
}

// all variants cause a copy of MyStruct
func getValue1(from optionalStruct: MyStruct?) -> Int? {
    switch optionalStruct {
        case .some(let myStruct):
            myStruct.value
        case .none: nil
    }
}

func getValue2(from optionalStruct: MyStruct?) -> Int? {
    optionalStruct.map { $0.value }
}

func getValue3(from optionalStruct: MyStruct?) -> Int? {
    if optionalStruct == nil {
        return nil
    } else {
        return optionalStruct.unsafelyUnwrapped.value
    }
}
func getValue4(from optionalStruct: MyStruct?) -> Int? {
    if optionalStruct == nil {
        return nil
    } else {
        return optionalStruct!.value
    }
}
func getValue5(from optionalStruct: MyStruct?) -> Int? {
    optionalStruct?.value
}

For example, getValue1 produces assembly like this (notice the copy):

output.getValue1(from: output.MyStruct?) -> Swift.Int?: # @"output.getValue1(from: output.MyStruct?) -> Swift.Int?"
        push    rbx
        sub     rsp, 80
        lea     rsi, [rsp + 40]
        call    outlined init with copy of output.MyStruct?
        cmp     qword ptr [rsp + 72], 0
        je      .LBB13_1
        movups  xmm0, xmmword ptr [rsp + 40]
        movups  xmm1, xmmword ptr [rsp + 56]
        movaps  xmmword ptr [rsp], xmm0
        mov     rax, qword ptr [rsp + 72]
        mov     qword ptr [rsp + 32], rax
        movaps  xmmword ptr [rsp + 16], xmm1
        mov     rbx, qword ptr [rsp]
        mov     rdi, rsp
        call    outlined destroy of output.MyStruct
        xor     edx, edx
        jmp     .LBB13_3
.LBB13_1:
        mov     dl, 1
        xor     ebx, ebx
.LBB13_3:
        mov     rax, rbx
        add     rsp, 80
        pop     rbx
        ret

In fact, all of the versions above generate a call to something like outlined init with copy of output.MyStruct?.

The only way I've found to avoid the copy is to use the underscore-prefixed method Optional._borrowingMap:

func getValue(from optionalStruct: MyStruct?, key: Int) -> Int? {
    optionalStruct._borrowingMap { $0.value }
}

Which produces much cleaner assembly without a copy:

output.getValue(from: output.MyStruct?, key: Swift.Int) -> Swift.Int?:
        cmp qword ptr [rdi + 32], 0
        je .LBB13_1
        mov rax, qword ptr [rdi]
        xor edx, edx
        ret

This appears to leverage the relatively new borrowing switch feature (SE-0432), but that capability is currently only available either for ~Copyable types or in a generic context where the type is not required to be Copyable.

4 Likes

interesting – thank you both for the examples. i looked into the lowering logic a little bit and it made me wonder if these cases may have a related root cause. it seems optional binding results in some codegen for switching over an enum element (makes sense), and the subject, at least in some cases, is passed to the switch at +1, which results in a copy. it's not clear to me whether that is out of necessity, or a matter of convenience, or what.

how did you rewrite it and verify there were no copies? i tried a few formulations using nested switches, but they all still seemed to end up with what looked like a bunch of copy_xyz SIL instructions.