Why are let and var variable lifetimes different?

test code:

class Person {
    deinit {
        print("Person deinit")
    }
}

class Student {
    deinit {
        print("Student deinit")
    }
}

func run() {
    let person = Person()
    var student = Student()
    print("run end")
}

run()

output result:
image

Encountered a problem, in release mode, let variables should be released before the end of "run end" like var variables, why are let variables released after "run end"?
Is there any reason here?

What optimization settings was this built with?


I'm using release mode.

Are you suggesting there is a bug or are you just curious why it is so? AFAIK nothing in the language specifies guarantees in this behaviour, so even two vars or two lets can behave differently. IIRC there was a (pre?) pitch thread here a couple of months ago that proposed to make variable lifetime more deterministic, until that - it is what it is.

While that's true I am still curious what's happening here. I wouldn't expect anything to delay object deinitialization in this situation.

What's quite interesting is that you can see it statically. I made a little demonstration using Godbolt.

In 5.0 and 5.1, the generated code looks like this:

main:
        push    rbp
        mov     rbp, rsp
        call    run_end@PLT
        call    in_student_deinit@PLT
        call    in_person_deinit@PLT
        xor     eax, eax
        pop     rbp
        ret

But in 5.2, it changed to this, for some reason swapping the order of "run end" and the student's deinit:

main:
        push    rbp
        mov     rbp, rsp
        call    in_student_deinit@PLT
        call    run_end@PLT
        call    in_person_deinit@PLT
        xor     eax, eax
        pop     rbp
        ret

Which is where it stayed all this time, including in 5.5 (the latest release on Godbolt). However, running it against nightly shows it has since been fixed:

main:
        push    rax
        call    run_end@PLT
        call    in_student_deinit@PLT
        call    in_person_deinit@PLT
        xor     eax, eax
        pop     rcx
        ret
4 Likes

Both let and var instances are declared inside function scope. So in mental model they should both deinit after print("run end"). I suppose ARC optimizations come into play when -O optimization enabled, so let instance is deallocated earlier.

Looks so. And this was the proposal to make behaviour more obvious and deterministic.

Thanks for your answer.
Yes, according to WWDC Session 10216, in release mode, it is true, if the variable is not used, it will be released as early as possible. So the let variable should also be released in advance. In fact, it is released after the "run end". Curious, what is the reason for Apple's design?

Thanks for your answer.I'm just curious, according to WWDC Session 10216, in release mode, if the variable is not used, it will be released as early as possible. So the let variable should also be released early, in fact it is released after the "run end".

Thanks for your answer.
I am going to download the latest Swift 5.6 to verify.

It’s a bit subtle, but by my interpretation that WWDC session doesn’t contradict the behavior here. The speaker notes that “depending on the ARC optimizations that kick in, the observed lifetimes may differ from their guaranteed minimum” (4:50). Furthermore, the following section describes that precise object lifetimes should not be relied upon—beyond the guarantee of “strong reference will not be released before last use,” Swift does not currently promise much about object lifetimes. Though, as @tera notes, some moves have been made towards tightening guarantees in this area in future versions of Swift.

3 Likes