Why does this code generate unspecialized type destructors?

The following code:

enum StringSource {
    
    case string(String)
    
    case staticString(StaticString)

}

struct Name {

    var first: StringSource?

    var last: StringSource?

}

func test(body: () -> Name) {
    body()
}

Generates the following assembly:

output.test(body: () -> output.Name) -> ():
        // ...
        call    (outlined destroy of output.StringSource?)
        // ...
        call    (outlined destroy of output.StringSource?)

outlined destroy of output.StringSource?:
        push    rbx
        mov     rbx, rdi
        lea     rdi, [rip + (demangling cache variable for type metadata for output.StringSource?)]
        call    __swift_instantiateConcreteTypeFromMangledName
        mov     rcx, qword ptr [rax - 8]
        mov     rdi, rbx
        mov     rsi, rax
        call    qword ptr [rcx + 8]
        mov     rax, rbx
        pop     rbx
        ret

I'm guessing this has something to do with the compiler's rules around when to inline and when to specialize, but it seems like an unnecessary penalty to me to have to make this destruction generic.

Is this a bug, a potential unimplemented optimization, or necessary for some other reason I'm missing?

Also, is there a way I can force Swift to use a specialized implementation?

EDIT:

This also happens with other type operations, like copies:

func test(name: Name) -> Name {
    name
}
output.test(name: output.Name) -> output.Name:
        // ...
        call    (outlined init with copy of output.StringSource?)
        // ...
        call    (outlined init with copy of output.StringSource?)
        // ...

outlined init with copy of output.StringSource?:
        // ...
        call    __swift_instantiateConcreteTypeFromMangledName
        // ...
2 Likes

Simpler demonstration of this issue, which seems to be triggered by reference types & generic types, despite all types being known at compile-time:

enum Box<T> { case some(T), none }

class Object { }

func destroyBoxedObject(_ pointer: UnsafeMutablePointer<Box<Object>>) {
    pointer.deinitialize(count: 1) // creates outlined generic destruction
}

func destroyObject(_ pointer: UnsafeMutablePointer<Object>) {
    pointer.deinitialize(count: 1) // inlined destruction (release)
}

struct Value { }

func destroyBoxedValue(_ pointer: UnsafeMutablePointer<Box<Value>>) {
    pointer.deinitialize(count: 1) // optimized away entirely
}
output.destroyBoxedObject(Swift.UnsafeMutablePointer<output.Box<output.Object>>) -> ():
        jmp     (outlined destroy of output.Box<output.Object>)

outlined destroy of output.Box<output.Object>:
        // ...
        lea     rdi, [rip + (demangling cache variable for type metadata for output.Box<output.Object>)]
        call    __swift_instantiateConcreteTypeFromMangledName
        // ...

output.destroyObject(Swift.UnsafeMutablePointer<output.Object>) -> ():
        mov     rdi, qword ptr [rdi]
        jmp     swift_release@PLT

output.destroyBoxedValue(Swift.UnsafeMutablePointer<output.Box<output.Value>>) -> ():
        ret

I'm not sure of the exact root cause, but it seems related to "type layout"-based VWT function generation. When compiled with -disable-type-layouts both of your examples get specialized destroy functions. (Compiler Explorer)

1 Like

Thank you for the investigation! It looks like this behavior has been around across several major Swift versions, which suggests to me that it's a long-standing issue.