Why does Swift decide to insert a retain/release pair here?

So I noticed that when a reference type is captured by a closure (even if that closure is non-escaping), Swift inserts retain/release calls around it:

public class Object { var count = 0 }

@inline(never)
func execute<T>(closure: () -> T) -> T {
    closure()
}

func compare(reference: Object, count: Int) -> Bool {
    // retain Object
    execute { reference.count == count }
    // release Object
}

This retain/release is eliminated if Swift is allowed to inline the closure:

//@inline(never)
func execute<T>(closure: () -> T) -> T {
    closure()
}

func compare(reference: Object, count: Int) -> Bool {
    // no retain of Object
    execute { reference.count == count }
    // no release of Object
}

or if the closure is explicitly eliminated entirely:

// no retain/release here:

func compare(reference: Object, count: Int) -> Bool {
    reference.count == count
}

Even though I would assume Swift should be able to safely optimize away the retain/release calls in both scenarios, inlining or not, the behavior here suggests to me that Swift is assuming that the closure might do things that could cause Object to be released, and inserts a retain/release call to protect against that. What are some of those things?

2 Likes

Ditto with no generics:

public class Object { var count = 0 }

@inline(never)
func execute(closure: () -> Void) {
    closure()
}

func compare(reference: Object, count: Int) {
    // retain Object
    execute { reference.count == count }
    // release Object
}

-O -enforce-exclusivity=unchecked

output.compare(reference: output.Object, count: Swift.Int) -> ():
        push    rbx
        mov     rbx, rdi
        call    swift_retain@PLT
        mov     rdi, rbx
        call    (function signature specialization <Arg[0] = Owned To Guaranteed, Arg[1] = Dead> of function signature specialization <Arg[0] = [Closure Propagated : closure #1 () -> () in output.compare(reference: output.Object, count: Swift.Int) -> (), Argument Types : [output.ObjectSwift.Int]> of output.execute(closure: () -> ()) -> ())
        mov     rdi, rbx
        pop     rbx
        jmp     swift_release@PLT
1 Like

not an answer to the 'why' behind the question, but it does appear that if the class is marked final, the retain/release pair is eliminated.

1 Like

Yeah, the optimizer should remove the retain/release pair.
I filed Unnecessary retain/release pair around closure · Issue #82155 · swiftlang/swift · GitHub

3 Likes

Thanks!