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


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

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

почему это?

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

2 153

2 ответа:

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

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

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

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 к одной специфической функции.

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