为什么不能通过堆栈指针访问捕获的C++本地引用变量?

5
我注意到编译器通过在堆栈上创建指向捕获本地变量的指针数组来实现引用捕获,可以将其传递给 lambda 表达式进行访问。这使我感到惊讶,因为编译器知道本地变量相对于堆栈指针的位置,所以我认为它可以直接传递堆栈指针。这样可以减少 lambda 表达式中的一次间接访问,并节省将指针放入堆栈的工作。我想知道为什么编译器不能那样做。
例如,这段 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;
}

在Clang 13 -O3下生成此汇编(注释为我的解释):

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 ()>&)

在GCC和MSVC上也类似。

2
Lambda是一个对象(匿名类的对象),并且捕获通过将它们作为该对象的(私有和非静态)成员变量来存储。这可能就是在这里发生的事情,lambda对象被存储在堆栈上,然后成员变量被初始化。 - Some programmer dude
3
std::function 并不是一个 lambda 表达式,它相当复杂且难以优化。如果你只使用 lambda 表达式,编译器可能会消除所有这些代码。 - Mat
@Someprogrammerdude 标准非常明确地未指定引用捕获变量在闭包对象中的表示方式。为什么编译器不将堆栈指针放入闭包类型中呢? - j6t
我想知道是否使用与编译器相关的标签(如g++等)可以增加获得有用回答的机会。你的问题最终归结为“为什么这些编译器不是这样做而是那样做?”这并不在C++标签的范围内。 - Peter
3
我相信你的问题可以通过重新表述为“为什么编译器实现者没有时间为每种可能的特殊情况实现优化?”来回答。 - molbdnilo
显示剩余2条评论
2个回答

0

你看到的不是未经优化的lambda表达式,而是围绕着std::function的所有东西。

如果你简化你的代码为:

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);
}

汇编变为

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

将所有内容完全优化并将结果作为常量传播。

区别在于:没有使用std :: function使用std :: function


这种看法是错误的:问题不是“为什么编译器不能优化掉所有东西”,而是“在编译器无法优化掉所有东西的情况下,为什么它们仍然不能通过将对局部变量的引用捕获优化为单个指针”。 - j6t

0
  1. 这是godbolt demo (稍微扩展了一下)。
  2. 我添加了testABI以显示您的函数参数是通过注册表传递的。
useTestABI():                        # @useTestABI()
        mov     edi, 3
        mov     esi, 5
        jmp     testABI(int, int)                    # TAILCALL
  1. 因此,在test2中,首先创建xy以在堆栈上创建本地变量,以便可以通过引用传递它们。
  2. 之后,在堆栈上创建lambda,因此将xy的地址保存到堆栈上。还将要在lambda使用时调用的函数的地址(无虚表的多态)添加到堆栈上的lambda中。

现在您不知道test将如何处理f。它可以克隆f,因此在这种情况下,必须将f从堆栈复制到其他位置。另一方面,当编译器编译test时,不知道传递了什么类型的lambda。创建副本可能具有副作用,也可能没有。因此,必须能够创建副本,并且必须与预定义的ABI兼容。

因此,您描述的这种快捷方式只有在编译器可以知道test的内容时才可行。当test的内容未知时,必须完全创建f,以使在需要时可以克隆它。


这个快捷方式...只有当编译器能够知道test的内容时才可能实现。我不同意。function类型无论如何都会擦除闭包类型;test对它的操作是无关紧要的。问题仍然存在:为什么编译器不能用一个单一指针来表示两个引用? - j6t
是啊,我想知道 test 可以做什么来防止使用堆栈指针。如果它克隆了 lambda,那么副本是否会引用相同的捕获局部变量,从而能够通过相同的指针访问它们? - Max Abernethy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接