Do empty closures allocate anything in Swift?

I am going to call a function:

func foo(_ completion: @escaping () -> Void) 

with

foo { }

Question: does { } allocate anything? There are no instructions, no captures, no parameters, so it might as well be described as a dynamically allocated structure:

struct {
    struct {} captures;
    void(*function)(void);
}

where for instantiation, captures would be an empty struct, size 0, and function would get assigned NULL; of course, C terms are inapplicable here, I know.

But in this example, allocating such a closure will take just 1 pointer to the function, and you would pass to the function:

  • an address to the struct
  • size_t of this struct

So how does swift handle this? Is it more efficient to make a closure optional and pass nil? Or empty closures cost exactly as much?

1 Like

Closure is reference type and its life cycle is managed by reference count. I think this holds true for an empty closure too. In this sense it’s perhaps more efficient to pass nil (that said, I doubt if this really matters in practice).

Yeah, but the question is, if there is a payload that's dynamically allocated, how large is it? Although, Optional.none will be stored on stack, without dynamic allocation, which makes it intrinsically better...
I would like to have a confirmation

I think the answer is yes. See this post. In my understanding the closure ‘s body also needs to be saved in text section. It would be great if someone can confirm it.

1 Like

Empty closures don't heap allocate. Taking @rayx's link to my post above, the structure is:

struct Closure {
    var functionPointer: UnsafeRawPointer
    var closureContext: AnyObject?
}

For your empty closure, this will be translated to roughly the following:

void yourFunc(void) {
}

Closure yourClosure = {
    .functionPointer = yourFunc,
    .closureContext = NULL
};

There will not be any heap allocation here.

9 Likes

Ohhh, and only the closure context is heap allocated? Not the closure itself? That's amazing!

But what about the function? Does my function actually get instantiated somewhere in the binary? Or-- is it also a null reference at runtime?

At a minimum an empty function needs a return instruction so it will take up some space in the binary. However LLVM has an optimization pass to merge identical functions so in theory if you have many such closures they won’t take up additional space. I wouldn’t worry about the code size penalty here — the fact that the context is null when the closure has no captures is the far more important optimization.

10 Likes