C++ Lambda生成的汇编代码

4

我有以下的C++初始代码:

class Lambda
{
public:
    int compute(int &value){
        auto get = [&value]() -> int {
            return 11 * value;
        };
        return get();
    }
};

int main(){
    Lambda lambda;
    int value = 77;
    return lambda.compute(value);
}

使用clang编译(使用-O1),生成以下汇编代码:

main: # @main
  push rax
  mov dword ptr [rsp + 4], 77
  mov rdi, rsp
  lea rsi, [rsp + 4]
  call Lambda::compute(int&)
  pop rcx
  ret
Lambda::compute(int&): # @Lambda::compute(int&)
  push rax
  mov qword ptr [rsp], rsi
  mov rdi, rsp
  call Lambda::compute(int&)::{lambda()#1}::operator()() const
  pop rcx
  ret
Lambda::compute(int&)::{lambda()#1}::operator()() const: # @Lambda::compute(int&)::{lambda()#1}::operator()() const
  mov rax, qword ptr [rdi]
  mov eax, dword ptr [rax]
  lea ecx, [rax + 4*rax]
  lea eax, [rax + 2*rcx]
  ret

问题:

  1. 在ASM中出现的{lambda()#1}是什么?据我所知,它可能是一个封装函数对象(即lambda主体)的闭包。请确认是否如此。
  2. compute()每次触发时是否生成一个新的闭包实例?还是同一个实例?

Lambda函数并没有从函数中逃逸出来,因此它不是回调或其他什么。它只是一个常规函数,在两个间接级别下被调用(指向指向'value'的指针,因此需要2次加载)。 - Peter Cordes
1
请注意,Lambda::compute(int&)::{lambda()#1}::operator()()是一个已解码的名称;您可能正在使用http://gcc.godbolt.org/而不是查看未经过滤的`clang -O1 -S输出。这里没有什么魔法,只是一个常规的call`指令。 - Peter Cordes
如果您将lambda闭包对象本身传递给非内联函数,则可能会得到类似于gcc为GNU C嵌套函数(访问包含函数中的变量)所做的代码。 https://dev59.com/7Gsy5IYBdhLWcg3w-zF5。但是希望您能获得比那些在堆栈上写入跳板指令字节的恶心可执行堆栈花招更有效的代码。 - Peter Cordes
3
@PeterCordes 我认为你永远不会理解那个疯狂的问题。Lambda表达式是匿名对象,它们的operator()本质上具有隐含的this指针作为上下文传递。可执行堆栈的疯狂操作是为了不传递上下文指针,因为当嵌套函数被用作指针时,它永远无法传递该指针。 - Passer By
@PasserBy:啊,对了,因为当你传递一个指针时,它必须像普通函数指针一样工作。我忘记了为什么需要跳板函数。但是,使用C++ lambda的所有内容都知道它是lambda,并知道将上下文传递给函数指针。所以,嵌套函数和lambda之间有趣的区别。 - Peter Cordes
显示剩余2条评论
2个回答

2
  1. compute中声明的lambda函数的实现体。
  2. 是的,在每次调用compute时,在概念上和实际上(在这个优化级别下1)都会在堆栈上创建一个新的闭包,并且相关的lambda函数将使用指向该闭包的指针(作为rdi传递,即与成员函数的this指针相同的方式作为第一个参数)进行调用。

1“在这个优化级别下”部分非常重要。实际上并没有任何需要编译器生成闭包或单独的lambda函数的东西。例如,在-O2时,clang会将所有这些内容优化掉,并直接在main()中返回答案作为常量。即使在-O1下,gcc也会进行相同的优化。


2
  1. 是的,调用lambda函数将需要生成一个闭包[除非编译器可以推断出它实际上没有被使用]

  2. 通过这种优化,每次调用都会调用compute,然后调用内部函数get(),它是compute函数内部的lambda函数。让编译器更高程度地优化这个情况,将优化掉这个调用 - 在我的尝试中,它将完全删除整个带有-O2的调用,并且只返回预先计算的常量847 - 正如您所期望的那样。对于更复杂的情况,它可能会或可能不会内联lambda部分但保留外部调用,反之亦然。这非常取决于涉及的函数内部发生的确切细节。

    只是为了明确,编译器正在按照您要求的方式执行:调用函数compute,然后调用函数get

添加

    int value2 = 88;
    int tmp = lambda.compute(value2);

在原问题中将代码插入到main函数中,基本上会对生成的代码进行以下更改(在Linux上使用clang++):
main:                                   # @main
    pushq   %rbx
    subq    $16, %rsp
    movl    $77, 12(%rsp)
    ## new line to set value2
    movl    $88, 8(%rsp)
    movq    %rsp, %rbx
    ## New line, passing reference of `value2` to lambda.compute
    leaq    8(%rsp), %rsi
    movq    %rbx, %rdi
    ## Call lambda.compute
    callq   _ZN6Lambda7computeERi
    ## Same as before.
    leaq    12(%rsp), %rsi
    movq    %rbx, %rdi
    callq   _ZN6Lambda7computeERi
    addq    $16, %rsp
    popq    %rbx
    retq

生成的代码

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