问题 绑定lambda的速度(通过std :: function)与functor struct的operator()


auto lam = [](int a, int b, int c) { return a < b && b < c; };

struct functor {
  int a;
  int b;
  bool operator()(int n) const { return a < n && n < b; }
};

在第一版中,我们

std::vector<std::function<bool (int)>> lamvals;
// get parameters and for each
lamvals.emplace_back(std::bind(lam, a, std::placeholders::_1, b));

替代方案是

std::vector<functor> lamvals;
// get parameters and for each
lamvals.emplace_back(functor{a, b});

在这两种情况下,我们都有一个简单的迭代

    return std::any_of(lamvals.cbegin(), lamvals.cend(),
            [n](const decltype(lamvals)::value_type & t){return t(n);});

我看到速度差为3:1,绑定的lambda更慢。仿函数几乎与存储整数对和硬编码测试一样快。显然,硬编码对于生产代码没那么有用,因为会有几个功能在起作用,而不仅仅是在它们之间。但是,我可以选择许多仿函数或许多lambdas。后者是更少的代码行,看起来更干净,但我认为我无法承受这种速度差异;此代码处于关键循环中。

我正在寻找加速建议。


5380
2017-09-23 00:54


起源

指某东西的用途 std::function 当然会慢一些。有了它,最有可能通过类型擦除来实现(归结为 virtual然后,与“普通”仿函数相比,它会导致显着的开销。 - Mark Garcia
将你的算符存储在一个 std::function 它将需要与lambda相同的时间。 - Oktalist
值得注意的是,lambdas并不比functor慢。代替, std::function 调用比常规函数调用慢。 - Mooing Duck


答案:


两种情况之间的区别基本上是使用仿函数,编译器确切地知道在编译时将调用什么,因此可以内联函数调用。有趣的是,Lambdas也有一种独特的类型。这意味着,当你在编译类型中使用lambda时(因为编译器必须知道所有类型),被调用的函数已经知道,因此可以进行内联。另一方面,函数指针仅基于其签名的类型。必须知道签名,以便可以适当地调用它并从中返回,但除了编译器所涉及的功能指针可以在运行时指向任何内容之外。关于std :: function也是如此。

将lambda包装在std :: function中时,从编译器的角度删除lambda的类型。如果这听起来很奇怪/不可能,那就这样想吧:因为固定类型的std :: function可以用相同的签名包装任何可调用的,所以编译器无法知道其他一些指令不会单独出现并且改变什么是std :: function包装。

这个链接, http://goo.gl/60QFjH,显示我的意思(顺便说一下,Godbolt页面非常方便,我建议熟悉它)。我在这里写了三个类似于你的例子。第一个使用std :: function包装lambda,第二个使用funl,第三个是使用decltype的裸lambda(unwrapped)。您可以查看右侧的程序集,看到后两者都内联,但不是第一个。

我的猜测是你可以使用lambdas做同样的事情。您可以使用a和b的lambdas进行基于值的捕获,而不是绑定。每次将lambda推回向量时,适当地修改a和b,然后瞧。

从风格上来说,我实际上强烈认为你应该使用结构。发生了什么事情要清楚得多。事实上,你似乎想要在一个地方捕获a和b,并在另一个地方测试c,这意味着你的代码中不仅仅在一个地方使用它。为了换取类似的2行代码,您可以获得更易读,更易于调试和更具可扩展性的内容。


15
2017-09-23 02:47



+1为 gcc.godbolt.org  - 非常酷! - Steve Lorimer
很有帮助。当我希望各种operator()都具有该签名时,您将如何声明该结构?虚拟将杀死内联。编译器是否接受std :: bind输出的decltype? - Andrew Lazarus
内联意味着零间接,但是拥有一个能够存储实际调用不同()的callables的容器意味着一定量的间接。这两个目标是不兼容的,缺少编译器非常聪明并且看着你的整个程序,你不能指望它做。对于结构体,您实际上可以通过使用final关键字来消除虚拟问题。但是,您的容器必须存储点而不是对象,因此仍然存在涉及指针解除引用,这通常会阻止内联。 - Nir Friedman
@NirFriedman:他们不是100%无法比拟......如果你知道在编译时会调用哪个,你可以使用元组。除此之外,是的。 - Mooing Duck
@Mooing Duck这是一个非常好的观点,感谢抓住它。 - Nir Friedman