问题描述
当我开发我的项目时,我发现 std::function 真的很慢。
所以我想知道为什么它真的很慢。
但我找不到明显的原因。
我认为 cpu 不能利用指令重新排序优化和 cpu 流水线,因为它不知道调用哪个函数是性能不佳的原因。
它使内存停滞和性能下降....
我说得对吗???
解决方法
包装到 std::function 中的代码总是比将代码直接内联到调用位置慢。特别是如果你的代码很短,比如 3-5 个 CPU 指令。
如果您的函数代码很大,有数百条指令,那么使用 std::function 或其他调用/包装代码的机制没有区别。
std::function 代码未内联。使用 std::function 包装器与在类中使用虚拟方法具有几乎相同的速度开销。不仅如此,std::function 机制看起来非常像虚拟调用机制,在这两种情况下,代码都没有内联,而是使用指向代码的指针通过汇编程序的 call
指令调用它。
如果你真的需要速度,那么使用 lambdas 并将它们作为模板参数传递,如下所示。如果可能,Lambda 总是内联的(如果编译器决定它会提高速度)。
#include <functional>
template <typename F>
void __attribute__((noinline)) use_lambda(F const & f) {
auto volatile a = f(13); // call f
// ....
auto volatile b = f(7); // call f again
}
void __attribute__((noinline)) use_func(
std::function<int(int)> const & f) {
auto volatile a = f(11); // call f
// ....
auto volatile b = f(17); // call f again
}
int main() {
int x = 123;
auto f = [&](int y){ return x + y; };
use_lambda(f); // Pass lambda
use_func(f); // Pass function
}
如果您查看上述示例的汇编代码(单击上面的在线试用链接),您会发现 lambda 代码已内联,而 std::function 代码未内联。
模板参数总是比其他解决方案更快,您应该始终在需要多态性同时具有高性能的任何地方使用模板。
,std::function
会导致几种开销。
首先,编译器很难理解。如果你有一个原始函数指针,一些编译器能够比使用 std 函数更容易“撤消”间接。但是,在这些情况下,原始函数指针和 std 函数的使用通常首先是错误的。
其次,std 函数的实现方式通常涉及一个虚函数表,它导致最多 2 个间接引用,而不是一个函数指针。当虚拟函数表脱离 CPU 缓存时,此命中最大。
第三,C++ 编译器非常擅长内联,以及通过 std 函数块间接调用。
现在,根据我的经验,当您进行缓冲区处理(例如逐像素操作)时,这种开销会变得最严重。
在这种情况下,您可以处理数百万或数十亿像素。每个像素完成的工作很小,与实际完成的工作相比,对每个操作进行 std 函数调用的开销最终会很大。
解决这个(及相关)问题的最简单方法是保存一个缓冲区处理函数,而不是像这样的每个元素的函数。
using Pixel = std::uint32_t;
using Scanline = std::span<Pixel>;
using ScanlineOp = std::function<void(Scanline)>;
template<class PixelOp>
ScanlineOp MakeScanlineOp( PixelOp op ) {
return [op=std::move(op)](Scanline line) {
for (Pixel& p : line)
op(p);
};
}
这里我采用逐像素操作,并将它与迭代代码一起保存到 std::function
中。
现在,在处理 4000 x 4000 像素的图像时,我没有遇到 std::function
1600 万次的开销,而是遇到了 4000 次。这将开销成本降低了 99.975%。
将某些东西的速度提高 4000 倍,您就不再关心它的成本了。
现在,std::span
是一种不在 c++11 中的类型。这是一个玩具版本:
template<class It>
struct range {
It b,e;
using reference = typename std::iterator_traits<It>::reference;
using value_type = typename std::iterator_traits<It>::value_type;
range( It s,It f ):b(s),e(f) {}
It begin() const { return b; }
It end() const { return e; }
bool empty() const { return begin()==end(); }
reference front() const { return *begin(); }
};
template<class It>
struct random_range:range<It> {
using range<It>::range;
using reference = typename range<It>::reference;
reference back() const { return *std::prev(this->end()); }
std::size_t size() const { return this->end()-this->begin(); }
reference operator[](std::size_t i) const{ return this->begin()[i]; }
};
template<class T>
struct array_view:random_range<T*> {
array_view( T* start,T* finish ):random_range<T*>(start,finish) {}
array_view( T* start,std::size_t length ):array_view(start,start+length) {}
array_view():array_view(nullptr,nullptr) {}
template<class C>
using data_type = typename std::remove_pointer< decltype( std::declval<C>().data() )>::type;
template<class U>
static constexpr bool pointer_compatible() {
return
std::is_same<
typename std::decay<U>::type,typename std::decay<T>::type
>::value
&& std::is_convertible<U*,T*>::value;
}
// accept any container whose
template<class C,typename std::enable_if< pointer_compatible<data_type<C>>(),bool >::type = true
>
array_view( C&& c ):array_view(c.data(),c.size()) {}
};
复杂的部分是我接受 vector
或 array
的地方,因为它的 .data()
字段存在,返回一个兼容的指针。
您将转换如下代码:
void foreachPixel( PixelOp op,Image img ) {
for (int i = 0; i < img.height(); ++i)
for (int j = 0; j < img.width(); ++j)
op(img[i][j]);
}
到
void foreachPixel( ScanlineOp op,Image img ) {
for (int i = 0; i < img.height(); ++i)
op(img.Scanline(i));
}
现在,我所展示的是针对一个具体案例。总体思路是,您可以将一些低级控制流注入您的 std::function
并在更高一级进行操作,从而消除几乎所有 std::function
开销。