Почему лямбды лучше оптимизируются компилятором, чем простые функции?

В своей книге The C++ Standard Library (Second Edition) Николай Йосуттис утверждает, что lambdas лучше оптимизируется компилятором, чем простые функции.

Кроме того, компиляторы С++ оптимизируют lambdas лучше, чем они делают обычные функции. (Страница 213)

Почему?

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

Ответ 1

Причина в том, что lambdas являются объектами функции, поэтому передача их в шаблон функции будет создавать новую функцию специально для этого объекта. Таким образом, компилятор может тривиально встроить лямбда-вызов.

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

В качестве примера рассмотрим следующий шаблон функции:

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 к одной конкретной функции.

Ответ 2

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