Why does the compiler always acquire the actor lock, even when not needed?

the compiler does not seem to be able to tell when a calling an isolated method that does not read or mutate the actor state on all code paths is the same as calling a nonisolated version of it that explicitly does not acquire the actor lock if not needed:

godbolt

public
actor A
{
    var i:Int

    init()
    {
        self.i = 0
    }
}
extension A
{
    nonisolated public
    func set1(i:Int) async
    {
        if i != 0
        {
            await self.set(i: i)
        }
    }
    public
    func set2(i:Int)
    {
        if i != 0
        {
            self.set(i: i)
        }
    }
    private
    func set(i:Int)
    {
        self.i = i
    }
}
public
func test1(a:A, i:Int) async
{
    await a.set1(i: i)
}

public
func test2(a:A, i:Int) async
{
    await a.set2(i: i)
}

output:

output.test1(a: output.A, i: Swift.Int) async -> ():
        mov     rcx, r14
        mov     qword ptr [r14 + 24], rsi
        mov     qword ptr [r14 + 16], rdi
        test    rsi, rsi
        je      .LBB14_2 // returns early without acquiring lock
        mov     rax, rdi
        lea     rdi, [rip + ((1) suspend resume partial function for output.test1(a: output.A, i: Swift.Int) async -> ())]
        mov     r14, rcx
        mov     rsi, rax
        xor     edx, edx
        jmp     swift_task_switch@PLT
.LBB14_2:
        mov     r14, rcx
        jmp     qword ptr [rcx + 8]

(1) suspend resume partial function for output.test1(a: output.A, i: Swift.Int) async -> ():
        push    rax
        mov     r13, qword ptr [r14 + 16]
        mov     rdi, qword ptr [r14 + 24]
        mov     rax, qword ptr [r13]
        call    qword ptr [rax + 80]
        mov     rax, r14
        pop     rcx
        jmp     qword ptr [rax + 8]
output.test2(a: output.A, i: Swift.Int) async -> ():
        mov     rax, rdi
        mov     qword ptr [r14 + 24], rsi
        mov     qword ptr [r14 + 16], rdi
        lea     rdi, [rip + ((1) suspend resume partial function for output.test2(a: output.A, i: Swift.Int) async -> ())]
        mov     rsi, rax
        xor     edx, edx
        jmp     swift_task_switch@PLT

(1) suspend resume partial function for output.test2(a: output.A, i: Swift.Int) async -> ():
        push    rax
        mov     rax, r14
        mov     rdi, qword ptr [r14 + 24]
        test    rdi, rdi
        je      .LBB17_2 // returns after acquiring lock
        mov     r13, qword ptr [rax + 16]
        mov     r14, rax
        mov     rax, qword ptr [r13]
        call    qword ptr [rax + 80]
        mov     rax, r14
.LBB17_2:
        mov     r14, rax
        pop     rcx
        jmp     qword ptr [rax + 8]

is this a missed optimization opportunity? is there a reason why test2 must acquire the actor lock on all paths?

1 Like