Search code examples
swiftdeferred

Swift defer statements internals


Lets assume very simple example:

func square() -> Int {
    var x = 5
    defer { x = 10 }
    return x
}

Why 5 is returned?

We know that defer is able to work only until rbp is vanished. So defer executes until return.

That how it look like in assembly:

output.square() -> Swift.Int:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     qword ptr [rbp - 8], 0
        mov     qword ptr [rbp - 8], 5
        lea     rdi, [rbp - 8]
        call    ($defer #1 () -> () in output.square() -> Swift.Int)
        mov     eax, 5
        add     rsp, 16
        pop     rbp
        ret

$defer #1 () -> () in output.square() -> Swift.Int:
        push    rbp
        mov     rbp, rsp
        mov     qword ptr [rbp - 8], 0
        mov     qword ptr [rbp - 8], rdi
        mov     qword ptr [rdi], 10
        pop     rbp
        ret

Do I got it right, that defer and return statements in example just use different registers. eax for return and rdi for defer.

And what happens when we use reference type

output.square() -> output.X:
        push    rbp
        mov     rbp, rsp
        push    r13
        sub     rsp, 24
        mov     qword ptr [rbp - 16], 0
        xor     eax, eax
        mov     edi, eax
        call    (type metadata accessor for output.X)
        mov     r13, rax
        call    (output.X.__allocating_init() -> output.X)
        mov     rdi, rax
        mov     qword ptr [rbp - 24], rdi
        call    swift_retain@PLT
        mov     rax, qword ptr [rbp - 24]
        mov     qword ptr [rbp - 16], rax
        lea     rdi, [rbp - 16]
        call    ($defer #1 () -> () in output.square() -> output.X)
        mov     rdi, qword ptr [rbp - 16]
        call    swift_release@PLT
        mov     rax, qword ptr [rbp - 24]
        add     rsp, 24
        pop     r13
        pop     rbp
        ret

type metadata accessor for output.X:
        lea     rax, [rip + (full type metadata for output.X)+16]
        xor     ecx, ecx
        mov     edx, ecx
        ret

$defer #1 () -> () in output.square() -> output.X:
        push    rbp
        mov     rbp, rsp
        push    r13
        sub     rsp, 24
        mov     qword ptr [rbp - 24], rdi
        mov     qword ptr [rbp - 16], 0
        mov     qword ptr [rbp - 16], rdi
        xor     eax, eax
        mov     edi, eax
        call    (type metadata accessor for output.X)
        mov     r13, rax
        call    (output.X.__allocating_init() -> output.X)
        mov     rcx, rax
        mov     rax, qword ptr [rbp - 24]
        mov     rdi, qword ptr [rax]
        mov     qword ptr [rax], rcx
        call    swift_release@PLT
        add     rsp, 24
        pop     r13
        pop     rbp
        ret

Solution

  • The assembly-language output is not particularly helpful in understanding what's happening here. Absent compiler bugs, the compiler generates assembly that matches the language's requirements, and is free to do it any way it likes. The specific registers involved do not matter to this analysis.

    The behavior precisely matches Swift's value type semantics. x is an Int, which is a value type. The line return x returns a copy of x. The current value of x is 5, so that is copied for return. Later, defer replaces x with a copy of 10. x is then thrown away.

    There's no problem being interested in the assembly output, of course. It actually can be enlightening in this case. Here is the same code with optimizations:

    output.square() -> Swift.Int:
            mov     eax, 5
            ret
    

    You'll note the 10 is nowhere to be found.