C++ 为什么std::函数太慢是因为CPU可以';不能利用指令重新排序?

C++ 为什么std::函数太慢是因为CPU可以';不能利用指令重新排序?,c++,c++11,C++,C++11,当我开发我的项目时,我发现std::function非常慢。 所以我想知道为什么它真的很慢。 但我找不到明显的原因 我认为Cpu不能利用指令重新排序优化和Cpu管道,因为它不知道调用哪个函数是性能差的原因。 它会导致内存暂停和性能降低 我说得对吗?包装在std::function中的代码总是比直接将代码内联到调用位置慢。特别是如果你的代码很短,比如3-5条CPU指令 如果函数的代码很大,有数百条指令,那么使用std::function还是其他调用/包装代码的机制就没有区别了 std::函数代码未

当我开发我的项目时,我发现std::function非常慢。
所以我想知道为什么它真的很慢。
但我找不到明显的原因

我认为Cpu不能利用指令重新排序优化和Cpu管道,因为它不知道调用哪个函数是性能差的原因。
它会导致内存暂停和性能降低


我说得对吗?

包装在std::function中的代码总是比直接将代码内联到调用位置慢。特别是如果你的代码很短,比如3-5条CPU指令

如果函数的代码很大,有数百条指令,那么使用std::function还是其他调用/包装代码的机制就没有区别了

std::函数代码未内联。使用std::函数包装器与在类中使用虚拟方法具有几乎相同的速度开销。不仅如此,std::function机制看起来非常像虚拟调用机制,在这两种情况下,代码都不是内联的,并且使用指向代码的指针用汇编程序的
call
指令调用它

如果您确实需要速度,那么使用lambda并将其作为模板参数传递,如下所示。若可能的话,lambda总是内联的(若编译器决定它将提高速度)

#包括
模板
void uuu属性uuu((noinline))使用lambda(F const&F){
自动易失性a=f(13);//调用f
// ....
自动易失性b=f(7);//再次调用f
}
void uuu属性uuu((noinline))使用函数(
标准::函数常数(f){
自动易失性a=f(11);//调用f
// ....
自动易失性b=f(17);//再次调用f
}
int main(){
int x=123;
自动f=[&](int y){返回x+y;};
使用λ(f);//传递λ
使用_func(f);//传递函数
}
如果您查看上面示例中的汇编代码(单击上面的TryItOnline链接),则可以看到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);
  };
}
复杂的部分是我接受
向量
数组
,因为它的
.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(像素点,图像img){
对于(int i=0;i

void foreachPixel(扫描线操作,图像img){
对于(int i=0;i

现在,我演示的是一个具体案例。一般的想法是,您可以将一些低级控制流注入到
std::function
中,并操作一个更高的级别,从而删除几乎所有的
std::function
开销。

std::function中包含的函数代码是小还是大?若它很小,比如3-5条CPU指令,那个么yes std::function会使它变慢,因为std::function并没有内联到外部调用代码中。您应该只使用lambda并将lambda作为模板参数传递给其他函数,lambda内联到调用代码中。如果您的包装函数代码很大,那么它是否在std::function wrapper中就没有区别了。does和help?1。向我们证明它很慢;2.定义什么是“慢”,这是因为std::函数的istance的拷贝。如果您的可调用的生存期允许您这样做,请考虑使用STD::REF明确地要求将函数作为引用(避免复制)传递。这可能是原因之一。也许不是。我们对您如何构建项目一无所知。您是否对其进行了优化?分析的结果是什么?请出示号码。谢谢。但如果我想存储可调用对象以便以后调用,std::function是不可避免的,对吧???@SungJinKang如果可以,你应该将函数存储为模板参数,并作为模板参数传递。看看上面的main()代码,我还将f()lambda存储在局部变量f中。这不是问题,它不会阻止内联。只需到处传递f作为模板参数。std::function应该仅用于多态情况,当您希望在不使用任何模板的情况下传递f()函数的不同变体时。我的意思是,如果我想在std中存储相同类型的各种函数:;向量,std::函数是不可避免的。对吗???@SungJinKang如果你想在std::vector中存储不同的lambda(不同的代码),那么是的
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()) {}
};
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));
}