Здрасте. Заметил, что С++11 — ые лямбды очень удобно использовать в качестве вложенных функций. Вот так:
int get_something(const SomeStruct& given)
{
// Считаем что-то большое, большой контекст локальных переменных.
// ...auto calculate_some = [&](int val)
{
// Считаем что-то из val, учитывая контекст внешней функции
// ...
};
// Используем эту вложенную функцию несколько раз
calculate_some(1);
calculate_some(2);
calculate_some(10);
// Какой-то результатreturn 1;
}
То есть суть тривиальна — полноценная вложенная функция с захватом контекста. Все работает. Однако меня беспокоит наличие потенциальных накладных расходов, связанных с таким подходом:
1. Может ли эта функция заинлайниться?
2. Дорого ли передавать "[&]" в качестве контекста? Просто передается указатель на стек или это может быть дороже?
3. Дорога ли сама иницализация лямбда-функтора? Понятно, что она вызывается единожды за вызов основной функции, но все же.
4. Какие еще могут быть подводные камни?
Здравствуйте, Went, Вы писали:
W>1. Может ли эта функция заинлайниться?
Может, а может и не заинлайниваться.
W>2. Дорого ли передавать "[&]" в качестве контекста? Просто передается указатель на стек или это может быть дороже? W>3. Дорога ли сама иницализация лямбда-функтора? Понятно, что она вызывается единожды за вызов основной функции, но все же.
Мне кажется, что лучше думать о лямбде, как об анонимном объекте, полями которого являются используемые сущности из захваченного контекста.
То, что этот объект будет создаваться в единственном месте, дает компилятору дополнительное поле деятельности для оптимизации.
Если этот объект используется на месте (а не передаваться куда-то дальше, например в std::function), то в оптимизированном коде,
скорее всего, не будет создаваться вообще никаких дополнительных объектов или оверхед будет сравним с ручной реализацией.
W>4. Какие еще могут быть подводные камни?
Некоторые рекомендуют указывать все захватываемые аргументы в явном виде вместо [&].
Для облегчения работы компилятору и большей определенности.
Говорить дальше не было нужды. Как и все космонавты, капитан Нортон не испытывал особого доверия к явлениям, внешне слишком заманчивым.
Здравствуйте, VTT, Вы писали:
VTT>Может, а может и не заинлайниваться.
Ну, понятно, это зависит целиком от компилятора. Вопрос в принципиальной возможности.
VTT>Мне кажется, что лучше думать о лямбде, как об анонимном объекте, полями которого являются используемые сущности из захваченного контекста.
Да, несомненно это объект, и именно так мы о нем и думаем.
VTT>Некоторые рекомендуют указывать все захватываемые аргументы в явном виде вместо [&].
А вот почему? Я сам так делал, а потом подумал — а зачем?
VTT>Для облегчения работы компилятору и большей определенности.
Вот тут самое интересное. Теоретически, указывая [&] я говорю компилятору: "захвати Stack Pointer и будь счастлив, все переменные можно будет вывести из смещений относительно этого адреса". Я понимаю, когда речь идет о [=], тут, понятно, объект-функция должен будет хранить все по значению. Или обычный компилятор тупо тянет в лямбду все внешние ссылки, которые он встретил при компиляции ее тела?
Здравствуйте, Went, Вы писали:
VTT>>Некоторые рекомендуют указывать все захватываемые аргументы в явном виде вместо [&]. W>А вот почему? Я сам так делал, а потом подумал — а зачем?
Но вот, например, захват какой-то переменной по ссылке прямо говорит, что эта переменная должна пережить эту лямбду. Если захватывать все сразу [&], то чтобы это понять, придется рассматривать тело функции.
Или можно случайно захватить или изменить что-то лишнее. Допустим, в контексте есть некоторый x, а в теле mutable лямбды должна быть локальная переменная x, а ее забывают объявить как переменную, или вообще забывают посчитать.
При рефакторинге (и вообще копипасте) можно легко напортачить.
W>Вот тут самое интересное. Теоретически, указывая [&] я говорю компилятору: "захвати Stack Pointer и будь счастлив, все переменные можно будет вывести из смещений относительно этого адреса". Я понимаю, когда речь идет о [=], тут, понятно, объект-функция должен будет хранить все по значению. Или обычный компилятор тупо тянет в лямбду все внешние ссылки, которые он встретил при компиляции ее тела?
Что-то я не думаю, что там вообще Stack Pointer в лямбду сохраняется. Тем более, что многие захватываемые переменные на самом деле будут существовать только в регистрах (к чему компилятор и стремится). Указание [&] скорее звучит как "Посмотри, что я там использую в теле лямбды из внешнего контекста и, при необходимости, сохрани это все на время жизни этой лямбды где-нибудь." Когда лямбда утилизируется на месте, то такой необходимости может и не появиться, или придется сохранять только кое-что. И не обязательно даже реально по ссылке.
Говорить дальше не было нужды. Как и все космонавты, капитан Нортон не испытывал особого доверия к явлениям, внешне слишком заманчивым.
Здравствуйте, VTT, Вы писали:
VTT>При рефакторинге (и вообще копипасте) можно легко напортачить.
Ну, в плане защиты от описок — да, согласен. Но в общем, если я вызываю лямбду локально и не передаю ее дальше?
VTT>Что-то я не думаю, что там вообще Stack Pointer в лямбду сохраняется. Тем более, что многие захватываемые переменные на самом деле будут существовать только в регистрах (к чему компилятор и стремится). Указание [&] скорее звучит как "Посмотри, что я там использую в теле лямбды из внешнего контекста и, при необходимости, сохрани это все на время жизни этой лямбды где-нибудь." Когда лямбда утилизируется на месте, то такой необходимости может и не появиться, или придется сохранять только кое-что. И не обязательно даже реально по ссылке.
Сейчас посмотрел что делает вижуаловский компилятор с "лямбдами — локальными функциями". Все достаточно оптимально. Использует общий esp, подразумевая, что функция вызывается только из текущего места.
Здравствуйте, niXman, Вы писали:
W>>Да, несомненно это объект, и именно так мы о нем и думаем.
X>ну, не всегда... X>если нет списка захвата — то просто локальная функция: X>
A>struct lambda {
A> static void impl() { f(); g(); }
A> void operator()() { f(); g(); }
A> decltype(&lambda::impl) operator() { return impl; }
A>};
A>
A>но вообще стандарт не говорит как именно это должно происходить. однако тип лямбда-выражения это всегда класс.
ну это же мошейничество со стороны компилятора...
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, niXman, Вы писали:
A>>вообще-то объект. с оператором приведения к указателю на функцию. X>как вот это: X>
X>struct lambda {
X> void operator()() {}
X>};
X>
X>можно привести к этому: X>
X>void(*fp)();
X>
X>? X>хочу пример.
Например так:
struct lambda
{
void operator()() {}
static void lambda_call_operator_invoker() // see ISO
{
return lambda{}();
}
using F = void (*)();
operator F() const
{
return lambda_call_operator_invoker;
}
};
int main()
{
void(*fp)() = lambda{};
}
Но даже если бы такого способа не было — это всё равно ничего не поменяло бы. Стандарт говорит что тип выражения лямбды — "unnamed nonunion class type", и при определённых обстоятельствах для этого типа доступно преобразование в указатель на функцию.
А уже как оно там реализовано внутри — дело десятое — например компилятор может не создавать оператор преобразования, а дёргать какие-нибудь внутренние интринсинки при необходимости
Здравствуйте, niXman, Вы писали:
W>>Да, несомненно это объект, и именно так мы о нем и думаем. X>ну, не всегда... X>если нет списка захвата — то просто локальная функция: X>
A>>но вообще стандарт не говорит как именно это должно происходить. однако тип лямбда-выражения это всегда класс. X>ну это же мошейничество со стороны компилятора...
это же код который генерируется, причем он даже не виден программисту.
нет никаких причин по которым он должен быть чистым, в т.ч. соблюдать DRY
Здравствуйте, Went, Вы писали:
W>Сейчас посмотрел что делает вижуаловский компилятор с "лямбдами — локальными функциями". Все достаточно оптимально. Использует общий esp, подразумевая, что функция вызывается только из текущего места.
Так с этого и надо было начинать. Есть сомнения — смотри, что выдает компилятор. Да и то, пока тебе профайлер это место не показал — можешь забить, пусть оно 100 раз неэффективно, но общих тормозов не дает.
Переубедить Вас, к сожалению, мне не удастся, поэтому сразу перейду к оскорблениям.
Здравствуйте, Ops, Вы писали:
Ops>Здравствуйте, Went, Вы писали:
W>>Сейчас посмотрел что делает вижуаловский компилятор с "лямбдами — локальными функциями". Все достаточно оптимально. Использует общий esp, подразумевая, что функция вызывается только из текущего места.
Ops>Так с этого и надо было начинать. Есть сомнения — смотри, что выдает компилятор. Да и то, пока тебе профайлер это место не показал — можешь забить, пусть оно 100 раз неэффективно, но общих тормозов не дает.
Кстати, и даже инлайнит. Причем, если вызов идет из цикла — то инлайнит, если дважды вызов руками — делает call.