Чому лямбди можуть бути оптимізовані компілятором, ніж звичайні функції?


171

У своїй книзі The C++ Standard Library (Second Edition)Ніколай Йосуттіс зазначає, що лямбди можна краще оптимізувати компілятором, ніж звичайні функції.

Крім того, компілятори C ++ оптимізують лямбдаси краще, ніж вони виконують звичайні функції. (Сторінка 213)

Чому так?

Я думав, що якщо мова йде про вкладиші, то більше не повинно бути різниці. Єдиною причиною, про яку я міг подумати, є те, що компілятори можуть мати кращий локальний контекст з лямбдами, і такі можуть зробити більше припущень та зробити більше оптимізацій.



В основному, твердження стосується всіх об'єктів функції , а не лише лямбда.
newacct

4
Це було б неправильно, оскільки покажчики функцій є і об’єктами функції.
Йоханнес Шауб - ліб

2
@litb: Я думаю, що я не погоджуюся з цим. wikipedia), люди мають на увазі клас екземпляра, який можна викликати, коли вони говорять про об'єкт функції.
Себастьян Мах

1
Деякі компілятори можуть краще оптимізувати лямбдаси, ніж звичайні функції, але не всі :-(
Коді Грей

Відповіді:


175

Причина полягає в тому, що лямбди є функціональними об'єктами тому передача їх у шаблон функції буде інстанціювати нову функцію спеціально для цього об’єкта. Таким чином, компілятор може тривіально вбудувати лямбда-дзвінок.

Для функцій, з іншого боку, застосовується старий застереження: вказівник функції передається до шаблону функцій, а у компіляторів традиційно виникає багато проблем з викликом викликів за допомогою функціональних покажчиків. Вони теоретично можуть бути накресленими, але лише в тому випадку, якщо навколишня функція також вкладена.

Як приклад, розглянемо наступний шаблон функції:

template <typename Iter, typename F>
void map(Iter begin, Iter end, F f) {
    for (; begin != end; ++begin)
        *begin = f(*begin);
}

Називаючи це лямбда таким чином:

int a[] = { 1, 2, 3, 4 };
map(begin(a), end(a), [](int n) { return n * 2; });

Результати цієї інстанції (створеної компілятором):

template <>
void map<int*, _some_lambda_type>(int* begin, int* end, _some_lambda_type f) {
    for (; begin != end; ++begin)
        *begin = f.operator()(*begin);
}

… Компілятор знає _some_lambda_type::operator ()і може вбудовувати дзвінки до нього тривіально. (А виклик функції mapз будь-якою іншою лямбдаю створив би нову інстанціюmap оскільки кожна лямбда має різний тип.)

Але при виклику за допомогою вказівника функції екземпляр виглядає так:

template <>
void map<int*, int (*)(int)>(int* begin, int* end, int (*f)(int)) {
    for (; begin != end; ++begin)
        *begin = f(*begin);
}

… І тут fвказується на іншу адресу для кожного виклику до, mapі тому компілятор не може вбудовувати дзвінки, fякщо оточуючий виклик mapтакож не був вкладений, щоб компілятор міг вирішити fодну конкретну функцію.


4
Можливо, варто згадати, що інстанціювання одного і того ж шаблону функції з іншим лямбда-виразом створить абсолютно нову функцію з унікальним типом, що може бути недоліком.
холод

2
@greggo Абсолютно. Проблема полягає в обробці функцій, які не можуть бути вписані (оскільки вони занадто великі). Тут виклик до зворотного дзвінка все ще може бути вкладений у випадку лямбда, але не у випадку вказівника функції. std::sortє класичним прикладом цього використання лямбда замість вказівника на функціональні функції, що приносить до семи разів (можливо, більше, але даних про це у мене немає!) збільшується.
Конрад Рудольф

1
@greggo Ви плутаєте дві функції тут: той , який ми передаємо лямбда до (наприклад std::sort, чи mapв моєму прикладі) , а сам лямбда. Лямбда зазвичай невелика. Інша функція - не обов'язково. Ми стурбовані вбудованими дзвінками лямбда всередині іншої функції.
Конрад Рудольф

2
@greggo я знаю. Це буквально те, що говорить останнє речення моєї відповіді.
Конрад Рудольф

1
Що мені цікаво (щойно натрапивши на нього), це те, що задана проста булева функція pred, визначення якої видно, і використання gcc v5.3, std::find_if(b, e, pred)не вбудовується pred, а std::find_if(b, e, [](int x){return pred(x);})робить. Кланг вдається встроїти обидва, але не створює код так швидко, як g ++ за допомогою лямбда.
rici

26

Тому що, коли ви передаєте "функцію" алгоритму, ви насправді передаєте вказівник на функцію, тому він повинен робити непрямий виклик через вказівник на функцію. Коли ви використовуєте лямбда, ви передаєте об'єкт в екземпляр шаблону, спеціально створений для цього типу, а виклик функції лямбда - це прямий дзвінок, а не виклик через покажчик функції, тому набагато більше шансів бути вкладеним.


5
"виклик функції лямбда - це прямий дзвінок" - справді. І те саме стосується всіх функціональних об'єктів, а не лише лямбда. Це просто функціональні вказівники, які взагалі не можна легко накреслити.
Піт Бекер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.