为什么编译器可以比普通函数更好地优化 lambdas?

Nicolai Josuttis 在其著作 The C++ Standard Library (Second Edition)中指出,编译器可以比普通函数更好地优化 lambdas。

此外,C + + 编译器优化 lambdas 比他们做的更好 普通功能。 (第213页)

为什么?

我认为,当涉及到内联应该没有任何区别了。我能想到的唯一原因是,编译器可能有一个更好的本地上下文与 lambdas,这样可以作出更多的假设和执行更多的优化。

25153 次浏览

原因是 lambdas 是 功能对象,因此将它们传递给函数模板将实例化特定于该对象的新函数。因此,编译器可以简单地内联 lambda 调用。

另一方面,对于函数,有一个老的警告: 函数 指针被传递给函数模板,编译器传统上有很多通过函数指针内联调用的问题。他们 可以理论上是内联的,但只有当周围的函数也是内联的。

例如,考虑以下函数模板:

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

用这样的 Lambda 来称呼它:

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 (),可以对它进行内联调用。(使用 任何其他 lambda 调用函数 map将创建 map的新实例化,因为每个 lambda 有一个不同的类型。)

但是当使用函数指针调用时,实例化看起来如下:

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解析为一个特定的函数。

因为当你把一个“函数”传递给一个算法时,你实际上是在传递一个函数指针,所以它必须通过函数指针进行间接调用。当你使用 lambda 的时候,你是在把一个对象传递给一个为这个类型特别实例化的模板实例,而对 lambda 函数的调用是一个直接调用,而不是通过一个函数指针的调用,所以更有可能是内联的。

Lambdas 并不比通常的函数快或慢。 如果错了请纠正我。

首先,lambda 和通常函数的区别是什么:

  1. Lambda 可以被捕获。
  2. 具有高可能性的 Lambda 在编译过程中将从目标文件中简单地删除,因为它有内部链接。

我们来谈谈抓捕。它不给函数任何性能,因为编译器必须传递额外的对象和处理捕获所需的数据。无论如何,如果你只是在地方使用 lambda 函数,它将很容易优化。而且如果 lambda 不使用捕获,你可以将你的 lambda 转换成一个函数指针。为什么?因为如果它没有捕获,那么它只是一个通常的函数。

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

上述两个例子都是有效的。

说到从目标文件中删除函数。您可以简单地将函数放到匿名名称空间中,这样就可以达成协议。函数将更乐于被内联,因为它不用于任何地方,除了您的文件。

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


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

上面的函数在性能上是相同的。

我还注意到函数指针和 lambda 是比较的。这不是一件好事,因为他们是不同的。当您有一个指向函数的指针时,它可以指向各种不同的函数,并且可以在运行时进行更改,因为它只是一个指向内存的指针。Lambda 不能这么做。它总是只操作一个函数,因为要调用哪个函数的信息存储在类型本身中。

你可以用这样的函数指针编写代码:

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

这绝对没问题,而且你不能用这种方式编写 lambdas 代码:

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

如果某些库接受函数指针,并不意味着编译器可以比普通函数更好地优化 lambdas,因为问题不在于通用库和函数指针。