Неэффективна ли стандартная сила С++ для привязки локальных переменных?
Мне недавно понадобилась лямбда, которая захватила несколько локальных переменных по ссылке, поэтому я сделал тестовый фрагмент, чтобы исследовать его эффективность, и скомпилировал его с помощью -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 ...
Ответы
Ответ 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, который включает в себя следующее:
В новой редакции больше не указаны элементы перезаписи или закрытия для записи "по ссылке". Использование сущностей, захваченных "по ссылке", влияет на исходные объекты, а механизм это достигается полностью для реализации.