为什么编译器可以比普通函数更好地优化Lambda表达式?

195
在他的书《C++标准库(第二版)》中,Nicolai Josuttis指出,与普通函数相比,编译器可以更好地优化lambda表达式。
“此外,C++编译器比普通函数更好地优化了lambda表达式。”(第213页)
为什么会这样呢?
我认为这可能是因为编译器在lambda表达式中有更好的本地上下文,从而可以做出更多的假设和执行更多的优化。当涉及到内联时,不应该再有任何区别。

相关 - iammilind
基本上,该语句适用于所有函数对象,而不仅仅是lambda。 - newacct
4
这会是不正确的,因为函数指针也是函数对象。 - Johannes Schaub - litb
2
@litb:我想我不同意那个观点。^W^W^W^W^W^W(查看标准后)我之前不知道这是C++的一种用法,虽然我认为在常规语言中(并且根据维基百科),当人们说“function object”时,他们指的是某个可调用类的实例。 - Sebastian Mach
1
一些编译器可以比普通函数更好地优化lambda表达式,但并非所有编译器都能做到 :-(。 - Cody Gray
3个回答

198

这是因为 lambda 是 函数对象,因此将它们传递给函数模板将会为该对象实例化一个新的函数。编译器可以轻松地内联 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解析为一个特定的函数。


5
值得一提的是,使用不同的lambda表达式实例化相同的函数模板将创建一个具有独特类型的全新函数,这可能是一个缺点。 - chill
3
@greggo 当处理无法内联的函数(因为它们太大)时,问题就出现了。在这种情况下,对回调函数的调用仍然可以在lambda的情况下进行内联,但在函数指针的情况下则不行。std::sort是这种情况的经典示例,使用lambda而不是函数指针可以带来七倍(可能更多,但我没有相关数据!)的性能提升。 - Konrad Rudolph
1
@greggo 你在混淆两个函数:一个是我们将lambda传递给的函数(例如std::sort或者我示例中的map),另一个是lambda本身。Lambda通常很小,而另一个函数则不一定。我们关心的是内联调用lambda在另一个函数内部。 - Konrad Rudolph
5
我知道。然而,这正是我回答中最后一句话的字面意思。 - Konrad Rudolph
1
我觉得很奇怪(刚刚偶然发现)的是,假设有一个简单的布尔函数pred,其定义是可见的,并且使用gcc v5.3,std::find_if(b, e, pred)不会内联pred,但是std::find_if(b, e, [](int x){return pred(x);})会。Clang可以内联两者,但是与带有lambda的g ++相比,它不能产生更快的代码。 - rici
显示剩余3条评论

29
因为当你将一个“函数”传递给算法时,实际上是传递了指向函数的指针,因此它必须通过指向函数的指针进行间接调用。当你使用 lambda 时,你传递给特定类型实例化的模板对象,对 lambda 函数的调用是直接调用,而不是通过函数指针的调用,因此更容易进行内联。

5
“对于 lambda 函数的调用是直接调用”- 的确如此。而且对于 所有 函数对象来说都是如此,不仅仅是 lambda 函数。只是函数指针可能无法像其他函数一样轻易地内联。 - Pete Becker

-2

Lambda表达式与普通函数的速度并不快或慢。如果有错误请纠正。

首先,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;
}

这完全没问题。您无法使用lambda表达式以这种方式编写代码:

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

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接