Why can't C++ locals captured by reference be accessed through the stack pointer?

Viewed 93

I noticed that compilers implement capture by reference by making an array on the stack of pointers to the captured locals, which can be passed to the lambda to access them. That surprised me because the compiler knows where the locals are relative to the stack pointer, so I thought it could just pass the stack pointer. That would make one less indirection in the lambda and save the work of putting the pointers on the stack. I was wondering why the compiler can't do that?

For example, this C++:

#include <functional>
extern void test(std::function<void()>& f);
int test2(int x, int y)
{
    std::function<void()> f([&]() { x += y; });
    test(f);
    return x;
}

generates this assembly on Clang 13 -O3 (comments mine):

mov     dword ptr [rsp + 8], edi    // put x on the stack
mov     dword ptr [rsp + 12], esi   // put y on the stack
lea     rax, [rsp + 8]
mov     qword ptr [rsp + 16], rax   // put &x on the stack
lea     rax, [rsp + 12]
mov     qword ptr [rsp + 24], rax   // put &y on the stack
mov     qword ptr [rsp + 40], offset std::_Function_handler<void (), test2(int, int)::$_0>::_M_invoke(std::_Any_data const&)
mov     qword ptr [rsp + 32], offset std::_Function_handler<void (), test2(int, int)::$_0>::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation)
lea     rdi, [rsp + 16]
call    test(std::function<void ()>&)

and similar on GCC and MSVC.

2 Answers

What you see is not the non optimized lambda, but all the stuff around std::function.

If you simplify your code to:

template < typename F>
void test(F& f )
{
    f();
}

int test2(int x, int y)
{
    auto f=[&]() { x += y; };
    test(f);
    return x;
}

int main()
{
    return test2(1,2);
}

The assembly becomes

test2(int, int):
        lea     eax, [rdi+rsi]
        ret
main:
        mov     eax, 3
        ret

Which is fully optimized out all the stuff and propagate the result as constant.

The the difference: without std::function vs with std::function

  1. Here is godbolt demo (extended a bit).
  2. I've added testABI to show that parameters to you function are passed by registry.
useTestABI():                        # @useTestABI()
        mov     edi, 3
        mov     esi, 5
        jmp     testABI(int, int)                    # TAILCALL
  1. So in test2 starts by creating x and y to create locals on stack so it would be possible to pass them by reference.
  2. After that lambda is created on stack, so addresses of x and y are saved to stack. Also addresses of functions to be invoked when lambda is used (polymorphism without vtable) are added to labda on stack

Now you do not know what test will do with f. It can clone f so in such case f have to be copied from stack to other place. Also on other side when test is compiled compiler do not know what kind of lambda was passed. Creating copy may have side effect or not. So creating a copy must be possible and must be compatible with predefined ABI.

So this shortcut you are describing is possible only when compiler could known content of test. When content of test is not know f must be fully created to make it possible to clone it when needed.

Related