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

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

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

Это почему?

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

3 ответа

Решение

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

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

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

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

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

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

Во-первых, в чем разница между лямбдой и обычной функцией:

  1. Лямбда может иметь захват.
  2. Лямбда с высокой вероятностью будет просто удалена при компиляции из объектного файла, так как имеет внутреннюю компоновку.

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

      void (*a1)() = []() {
    // ...
};
void _tmp() {
    // ...
}
void (*a2)() = _tmp;

Оба приведенных выше примера действительны.

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

      auto a1 = []() {
    // ...
};

namespace {
    auto a2() {
        // ...
    }
}

Функции выше будут одинаковыми по производительности.

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

Вы можете написать код с такими указателями функций:

      void f1() {
    // ...
}
void f2() {
    // ...
}
int main() {
    void (*a)();
    a = f1;
    a = f2;
}

Это абсолютно нормально. И вы не можете писать код с лямбда-выражениями таким образом:

      int main() {
    auto f1 = []() {
        // ...
    };
    auto f2 = []() {
        // ...
    };
    f2 = f1; // error: no viable overloaded '='
}

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

Другие вопросы по тегам