为啥编译器可以比普通函数更好地优化 lambda?

Posted

技术标签:

【中文标题】为啥编译器可以比普通函数更好地优化 lambda?【英文标题】:Why can lambdas be better optimized by the compiler than plain functions?为什么编译器可以比普通函数更好地优化 lambda? 【发布时间】:2012-11-23 05:33:00 【问题描述】:

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

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

这是为什么呢?

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

【问题讨论】:

Related. 基本上,该语句适用于所有函数对象,而不仅仅是lambdas。 那是不正确的,因为函数指针也是函数对象。 @litb:我认为我不同意这一点。^W^W^W^W^W^W(在查看标准之后)我不知道 C++ 主义,虽然我以通俗的说法(根据***)思考,人们在说函数对象时指的是一些可调用类的实例。 一些编译器可以比普通函数更好地优化 lambda,but not all :-( 【参考方案1】:

原因是 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 () 并且可以简单地内联对它的调用。 (并且使用 any 其他 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 表达式实例化同一个函数模板将创建一个具有唯一类型的全新函数,这很可能是一个缺点。 @greggo 绝对。问题是在处理无法内联的函数时(因为它们太大)。在 lambda 的情况下,对回调的调用仍然可以内联,但在函数指针的情况下不能。 std::sort 是使用 lambdas 而不是函数指针的经典示例,这里的性能提高了七倍(可能更多,但我没有数据!)。 @greggo 您在这里混淆了两个函数:我们将 lambda to 传递的一个(例如 std::sort,或在我的示例中为 map)和 lambda本身。 lambda 通常很小。另一个功能——不一定。我们关心的是内联调用到另一个函数中的 lambda @greggo 我知道。不过,这实际上是我回答的最后一句话所说的。 我觉得奇怪(刚刚偶然发现)是给定一个简单的布尔函数pred,其定义可见,并且使用gcc v5.3,std::find_if(b, e, pred)不会内联pred ,但std::find_if(b, e, [](int x)return pred(x);) 可以。 Clang 设法内联两者,但生成代码的速度不如使用 lambda 的 g++。【参考方案2】:

因为当您将“函数”传递给算法时,您实际上是在传递指向函数的指针,因此它必须通过指向函数的指针进行间接调用。当您使用 lambda 时,您将一个对象传递给专门为该类型实例化的模板实例,并且对 lambda 函数的调用是直接调用,而不是通过函数指针进行的调用,因此更有可能是内联的。

【讨论】:

“对 lambda 函数的调用是直接调用”——确实如此。 all 函数对象也是如此,而不仅仅是 lambda。它只是函数指针,如果有的话,就不能那么容易地内联了。【参考方案3】:

Lambda 并不比普通函数快或慢。 如有错误请指正。

首先,lambda和普通函数有什么区别:

    Lambda 可以进行捕获。 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 '='

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

【讨论】:

以上是关于为啥编译器可以比普通函数更好地优化 lambda?的主要内容,如果未能解决你的问题,请参考以下文章

C++ 编译器优化 - 为啥需要 constexpr?

为啥在 C++ 中将较大函数中的某些功能编写为 lambdas?

lambda 比 python 中的函数调用慢,为啥

为啥抛出异常比返回错误代码更好?

为啥使用 lambda 函数?

为啥 MySQL JOIN 比 WHERE IN (子查询) 快得多