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

PBS*_*PBS 11 c++ lambda compiler-optimization language-lawyer

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

void do_something_with(void*);

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

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

    do_something_with((void*)&func);
}
Run Code Online (Sandbox Code Playgroud)
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  ...
Run Code Online (Sandbox Code Playgroud)

显然,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)++;
};
Run Code Online (Sandbox Code Playgroud)

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

我的问题:

  1. clang和gcc都这样做的事实-O3使我怀疑标准中的某些东西实际上迫使闭包被低效地实现.是这样的吗?

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

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


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

struct X
{
    int a = 0, b = 0, c = 0;
} x;

auto func = [&x] () {
    x.a++;
    x.b++;
    x.c++;
};
Run Code Online (Sandbox Code Playgroud)
movl   $0x0,0x8(%rsp)
movl   $0x0,0xc(%rsp)
movl   $0x0,0x10(%rsp)

lea    0x8(%rsp),%rax
mov    %rax,(%rsp)

lea    (%rsp),%rdi
callq  ...
Run Code Online (Sandbox Code Playgroud)

Sha*_*our 6

它看起来像未指定的行为.C++ 14草案标准的以下段落:N3936部分5.1.2Lambda Expressions [expr.prim.lambda]让我想到:

如果实体是隐式或显式捕获但未通过复制捕获,则通过引用捕获实体.未指定是否在闭包类型中为通过引用捕获的实体声明了其他未命名的非静态数据成员.[...]

对于通过副本捕获的实体而言不同:

lambda表达式的复合语句中的每个id表达式(由副本捕获的实体的odr-use(3.2))将转换为对闭包类型的相应未命名数据成员的访问.

感谢dyp指出了一些我错过的相关文档.它看起来像缺陷报告750:对仅参考闭包对象的实现约束提供了当前措辞的基本原理,它说:

考虑一个例子:

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

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

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

新措辞不再指定"按引用"捕获的任何重写或关闭成员."通过引用"捕获的实体的使用会影响原始实体,实现此目的的机制完全留给实现.


归档时间:

查看次数:

462 次

最近记录:

10 年,8 月 前