What's stored at [AsyncContext + 16]?

@nsc and I have been looking at the assembly the compiler generates for an async function call and trying to understand it. Take this example (Godbolt link):

func caller() async {
    await asyncFunc()
}

@inline(never) func asyncFunc() async {
}

The assembly for the first partial func of caller (i.e. up to the await call) looks like this (commented by me).

output.caller() async -> ():
        push    rax
        ; Load size of AsyncContext required for calling asyncFunc()
        mov     edi, dword ptr [rip + (async function pointer to output.asyncFunc() async -> ())+4]
        ; Allocate AsyncContext for async function call.
        call    swift_task_alloc@PLT

        ; Store new AsyncContext at [current AsyncContext + 16]
        ; ??? Why???
        mov     qword ptr [r14 + 16], rax

        ; Set current AsyncContext as parent of new AsyncContext (parent field is at offset 0)
        mov     qword ptr [rax], r14
        ; Set continuation in new AsyncContext (field at offset 8)
        lea     rcx, [rip + ((1) await resume partial function for output.caller() async -> ())]
        mov     qword ptr [rax + 8], rcx
        ; Set new AsyncContext as the current AsyncContext (passed in r14)
        mov     r14, rax
        pop     rax
        ; Perform async function call (tail call)
        jmp     (output.asyncFunc() async -> ())

I think I understand every line of this except this one:

mov     qword ptr [r14 + 16], rax

What's the purpose of this? This seems to be storing the newly allocated AsyncContext (in rax) in the third field of the current AsyncContext (in r14). Looking at the class definition for AsyncContext, I don't see a third field, and I couldn't find an obvious match in any of its subclasses, either.

3 Likes

The first two fields are the type and retain count, so this is the first declared field, the parent context.

Really? AsyncContext is a C++ class, so it doesn't have a retain count, does it?

AsyncContexts are not reference-counted, no.

The AsyncContext structure is just the header on an async stack frame. Just like a C stack frame, the contents of the rest of the frame are opaque and function-specific; the function allocates whatever storage it needs there, including spilling values that it has to be able to use across the suspension. It shouldn't need to spill the child context pointer across the call — it gets passed into the continuation — but I assume the frame lowering code has somehow forgotten about that.

3 Likes

Aargh, sorry! Not sure how I convinced myself otherwise—did I think it was a Swift class, or did I think it was a C++ class that had the HeapObject layout as a base class? Thanks for correcting me.

3 Likes

Thanks @John_McCall!

It's worth a bug that we're spilling that — it's probably costing us a lot of code size.

4 Likes

I filed a bug:

2 Likes