C ++标准是否强制局部变量的引用是低效的?

时间:2015-07-09 13:20:55

标签: c++ lambda language-lawyer compiler-optimization

我最近需要一个通过引用捕获多个局部变量的lambda,因此我制作了一个测试片段来研究它的效率,并使用clang 3.6编译它-O3

void do_something_with(void*);

void test()
{
    int a = 0, b = 0, c = 0;

    auto func = [&] () {
        a++;
        b++;
        c++;
    };

    do_something_with((void*)&func);
}
movl   $0x0,0x24(%rsp)
movl   $0x0,0x20(%rsp)
movl   $0x0,0x1c(%rsp)

lea    0x24(%rsp),%rax
mov    %rax,(%rsp)
lea    0x20(%rsp),%rax
mov    %rax,0x8(%rsp)
lea    0x1c(%rsp),%rax
mov    %rax,0x10(%rsp)

lea    (%rsp),%rdi
callq  ...

显然,lambda只需要其中一个变量的地址,所有其他变量都可以通过相对寻址获得。

相反,编译器在堆栈上创建了一个包含指向每个局部变量的指针的结构,然后将结构的地址传递给lambda。它与我写作的方式大致相同:

int a = 0, b = 0, c = 0;

struct X
{
    int *pa, *pb, *pc;
};

X x = {&a, &b, &c};

auto func = [p = &x] () {
    (*p->pa)++;
    (*p->pb)++;
    (*p->pc)++;
};

由于各种原因,这是低效的,但最令人担忧的是,如果捕获了太多变量,它可能会导致堆分配。

我的问题:

  1. clang和gcc都在-O3执行此操作这一事实使我怀疑标准中的某些内容实际上会强制执行闭包效率低下。是这种情况吗?

  2. 如果是这样,那么基于什么推理?它不能用于编译器之间lambdas的二进制兼容性,因为任何知道lambda类型的代码都保证位于相同的转换单元中。

  3. 如果没有,那么为什么两个主要编译器缺少这种优化?

  4. 编辑:
    以下是我希望从编译器中看到的更高效代码的示例。这段代码使用较少的堆栈空间,lambda现在只执行一个指针间接而不是两个,并且lambda的大小不会增加捕获变量的数量:

    struct X
    {
        int a = 0, b = 0, c = 0;
    } x;
    
    auto func = [&x] () {
        x.a++;
        x.b++;
        x.c++;
    };
    
    movl   $0x0,0x8(%rsp)
    movl   $0x0,0xc(%rsp)
    movl   $0x0,0x10(%rsp)
    
    lea    0x8(%rsp),%rax
    mov    %rax,(%rsp)
    
    lea    (%rsp),%rdi
    callq  ...
    

1 个答案:

答案 0 :(得分:6)

看起来像是未指明的行为。 C++14 draft standard: N3936部分5.1.2 Lambda Expressions [expr.prim.lambda] 中的以下段落让我想到了这一点:

  

如果实体是隐式或显式的,则通过引用捕获实体   已捕获但未通过副本捕获。没有具体说明   在闭包中声明了其他未命名的非静态数据成员   通过引用捕获的实体的类型。 [...]

与副本捕获的实体不同:

  

a的复合语句中的每个id表达式   lambda-expression,它是由一个实体捕获的odr-use(3.2)   copy被转换为对相应未命名数据的访问   闭包类型的成员。

感谢dyp指出了一些我错过的相关文件。看起来defect report 750: Implementation constraints on reference-only closure objects提供了当前措辞的基本原理,它说:

  

考虑一个例子:

void f(vector<double> vec) {
  double x, y, z;
  fancy_algorithm(vec, [&]() { /* use x, y, and z in various ways */ });
}
     

5.1.2 [expr.prim.lambda]第8段要求此lambda的闭包类有三个引用成员,第12段   要求它从std :: reference_closure派生,暗示两个   其他指针成员。虽然8.3.2 [dcl.ref]第4段   允许在不分配存储的情况下实现引用,   当前的ABI要求将引用实现为指针。该   这些要求的实际效果是闭包对象   这个lambda表达式将包含五个指针。如果不是这些   但是,要求可以实现封闭   object作为指向堆栈帧的单个指针,生成数据   在函数调用操作符中作为相对于的偏移量进行访问   帧指针。目前的规范过于严格。

回应了关于允许潜在优化的确切要点,并作为N2927的一部分实施,其中包括以下内容:

  

新措辞不再指定&#34;通过引用&#34;的任何重写或关闭成员。捕获。   捕获的实体的使用&#34;通过引用&#34;影响原始实体和机制   实现这一点完全取决于实施。