Подтвердить что ты не робот

Неэффективна ли стандартная сила С++ для привязки локальных переменных?

Мне недавно понадобилась лямбда, которая захватила несколько локальных переменных по ссылке, поэтому я сделал тестовый фрагмент, чтобы исследовать его эффективность, и скомпилировал его с помощью -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);
}

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  ...

Очевидно, что лямбда нужен только адрес одной из переменных, из которой все остальные могут быть получены путем относительной адресации.

Вместо этого компилятор создал структуру в стеке, содержащую указатели на каждую локальную переменную, а затем передал адрес структуры в лямбда. Это так же, как если бы я написал:

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

Это неэффективно по разным причинам, но наиболее тревожно, потому что это может привести к распределению кучи, если захвачено слишком много переменных.

Мои вопросы:

  • Тот факт, что и clang, и gcc делают это при -O3, заставляет меня подозревать, что что-то в стандарте фактически заставляет блокировки быть реализованы неэффективно. Это дело?

  • Если да, то для каких рассуждений? Это не может быть для двоичной совместимости lambdas между компиляторами, потому что любой код, который знает о типе лямбда, гарантированно лежит в одной и той же единицы перевода.

  • Если нет, то почему эта оптимизация отсутствует у двух основных компиляторов?


EDIT:
Ниже приведен пример более эффективного кода, который я хотел бы видеть из компилятора. Этот код использует меньше пространства стека, теперь лямбда выполняет только одно указательное направление вместо двух, а размер лямбда не увеличивается в количестве захваченных переменных:

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  ...
4b9b3361

Ответ 1

Похоже на неуказанное поведение. Следующий абзац из С++ 14 draft standard: N3936 раздел 5.1.2 Lambda Expressions [expr.prim.lambda] заставляет меня думать так:

Объект фиксируется ссылкой, если он неявно или явно захвачен, но не захвачен копией. Неизвестно, дополнительные неназванные нестатические элементы данных объявляются в закрытии тип для объектов, захваченных ссылкой. [...]

который отличается для объектов, захваченных копией:

Каждое id-выражение в составной формуле лямбда-выражение, которое является неприемлемым (3.2) субъектом, захваченным копия преобразуется в доступ к соответствующим неназванным данным член типа замыкания.

Спасибо dyp за указание некоторых соответствующих документов, которые я как-то пропустил. Это выглядит как отчет о дефекте 750: Ограничения реализации для объектов замыкания только для ссылок дает обоснование для текущей формулировки, и он говорит:

Рассмотрим пример типа:

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, требует, чтобы класс закрытия для этой лямбда имел три контрольных элемента, а пункт 12 требует, чтобы он был получен из std:: reference_closure, подразумевая два дополнительные элементы указателя. Хотя пункт 8.3.2 [dcl.ref], пункт 4 позволяет использовать ссылку без выделения хранилища, текущие ABI требуют, чтобы ссылки выполнялись как указатели. практический эффект этих требований заключается в том, что объект закрытия для это лямбда-выражение будет содержать пять указателей. Если не для этих однако, было бы возможно осуществить закрытие объект как единственный указатель на стек стека, генерирующий данные обращается к оператору вызова функции как к смещению относительно указатель рамки. Текущая спецификация слишком жестко ограничена.

который отражает ваши точные указания о возможности оптимизации и был реализован как часть N2927, который включает в себя следующее:

В новой редакции больше не указаны элементы перезаписи или закрытия для записи "по ссылке". Использование сущностей, захваченных "по ссылке", влияет на исходные объекты, а механизм это достигается полностью для реализации.