std::function 的性能开销是多少?

What is the performance overhead of std::function?

提问人: 提问时间:2/20/2011 最后编辑:Craig M. Brandenburg 更新时间:9/20/2021 访问量:53415

问:

我在一个论坛上听说使用会导致性能下降。是真的吗?如果属实,性能会大幅下降吗?std::function<>

C++ Boost 标准

评论

38赞 Fred Nurk 2/20/2011
什么替代方案相比,导致性能下降?
5赞 Lightness Races in Orbit 2/20/2011
你必须比这更具体,user408141。
7赞 sbi 2/20/2011
真的,这是一个糟糕的问题。
2赞 2/20/2011
哦对不起,我太笨拙了!:D
2赞 SJHowe 4/30/2022
我不认为这是一个糟糕的问题。因为我会调用一个普通的自由函数,并想知道与 std::function 有什么区别。由于您可以用自由函数、成员函数、函子、std::bind 的结果、lamdas 填充 std::function,因此我会在性能时间和生成的代码方面与它们中的每一个进行比较

答:

19赞 UncleBens 2/20/2011 #1

您可以从 boost 的参考资料中找到信息:通过 boost::function 调用会产生多少开销?性能

这并不能确定“是或否”来提升功能。考虑到程序的要求,性能下降可能是可以接受的。通常情况下,程序的某些部分对性能不重要。即便如此,它也可能是可以接受的。这只能由您决定。

至于标准库版本,该标准仅定义了一个接口。这完全取决于各个实现来使其工作。我想会使用与 boost 函数类似的实现。

14赞 lurscher 2/20/2011 #2

这在很大程度上取决于您是否在不绑定任何参数(不分配堆空间)的情况下传递函数。

还取决于其他因素,但这是主要因素。

的确,你需要一些东西来比较,你不能简单地说,与根本不使用它相比,它“减少了开销”,你需要将它与使用传递函数的替代方式进行比较。如果你可以完全不使用它,那么从一开始就不需要它

评论

4赞 underscore_d 5/15/2017
如果实现使用小缓冲区优化将函数对象存储在实例中,并且传递的可调用对象在适合 SBO 的大小范围内,则即使是绑定参数也可能不会产生动态分配。std::function
15赞 Sebastian Mach 1/18/2012 #3

首先,随着函数内部的增加,开销变小;工作负载越高,开销越小。

其次:g++ 4.5 与虚函数相比没有任何区别:

main.cc

#include <functional>
#include <iostream>

// Interface for virtual function test.
struct Virtual {
    virtual ~Virtual() {}
    virtual int operator() () const = 0;
};

// Factory functions to steal g++ the insight and prevent some optimizations.
Virtual *create_virt();
std::function<int ()> create_fun();
std::function<int ()> create_fun_with_state();

// The test. Generates actual output to prevent some optimizations.
template <typename T>
int test (T const& fun) {
    int ret = 0;
    for (int i=0; i<1024*1024*1024; ++i) {
        ret += fun();
    }    
    return ret;
}

// Executing the tests and outputting their values to prevent some optimizations.
int main () {
    {
        const clock_t start = clock();
        std::cout << test(*create_virt()) << '\n';
        const double secs = (clock()-start) / double(CLOCKS_PER_SEC);
        std::cout << "virtual: " << secs << " secs.\n";
    }
    {
        const clock_t start = clock();
        std::cout << test(create_fun()) << '\n';
        const double secs = (clock()-start) / double(CLOCKS_PER_SEC);
        std::cout << "std::function: " << secs << " secs.\n";
    }
    {
        const clock_t start = clock();
        std::cout << test(create_fun_with_state()) << '\n';
        const double secs = (clock()-start) / double(CLOCKS_PER_SEC);
        std::cout << "std::function with bindings: " << secs << " secs.\n";
    }
}

impl.cc

#include <functional>

struct Virtual {
    virtual ~Virtual() {}
    virtual int  operator() () const = 0;
};
struct Impl : Virtual {
    virtual ~Impl() {}
    virtual int  operator() () const { return 1; }
};

Virtual *create_virt() { return new Impl; }

std::function<int ()> create_fun() { 
    return  []() { return 1; };
}

std::function<int ()> create_fun_with_state() { 
    int x,y,z;
    return  [=]() { return 1; };
}

输出:g++ --std=c++0x -O3 impl.cc main.cc && ./a.out

1073741824
virtual: 2.9 secs.
1073741824
std::function: 2.9 secs.
1073741824
std::function with bindings: 2.9 secs.

所以,不要害怕。如果您的设计/可维护性可以通过首选虚拟呼叫来提高,请尝试一下。就我个人而言,我真的很喜欢不在我的类的客户端上强制接口和继承的想法。std::function

评论

3赞 Sebastian Mach 1/18/2012
@Xeo:没错。但验证胜于相信:)当您不使用优化时,相同的测试显示 1:3 的差异,因此此测试并非完全没有道理。std::function
2赞 MWB 2/3/2014
使用 G++ 4.8.2,我始终获得 2.9、3.3 和 3.3 秒。如果我添加它们,它们都变成 3.3。我完全疯狂的猜测是,GCC 实际上试图优化(类似于虚拟函数),但优化实际上很痛苦。-fltostd::function-flto
4赞 Paul Brannan 6/21/2016
使用 g++ 5.3,我得到 2.0、2.3、2.3 (-O2);0.7, 2.0, 2.0 (-O2 -flto);2.3、2.3、2.3 (-O2 -flto -fno-去虚拟化);2.0、2.3、2.3 (-O2 -fno-devirtualize)。因此,较新的 g++ 版本中的去虚拟化似乎已经得到了足够的改进,这不再是去优化。
4赞 Alexandre Pereira Nunes 3/15/2017
g++ 6.3.0:g++ -std=gnu++14 -O3 -flto -march=native impl.cpp main.cpp && ./a.out 1073741824 virtual:1.97619 秒。 1073741824 std::function:6.86855 秒。 1073741824 std::function 带绑定:6.86847 秒。
3赞 Andrej Kesely 5/30/2019
Ubuntu 18.04 (AMD 2400G) 上的 g++ 7.4.0:' g++ --std=c++17 -O3 impl.cc main.cc && ./a.out': virtual: 1.38742 秒, std::function: 1.44681 秒, std::function with bindings: 1.39367 secs.
110赞 Cassio Neri 2/1/2012 #4

事实上,在使用它时必须考虑性能问题。的主要优势,即它的类型擦除机制,不是免费的,我们可能(但不一定必须)为此付出代价。std:functionstd::function

std::function是包装可调用类型的模板类。但是,它不会对可调用类型本身进行参数化,而只会对它的返回和参数类型进行参数化。可调用类型仅在构造时是已知的,因此,不能有此类型的预先声明的成员来保存给定给其构造函数的对象的副本。std::function

粗略地说(实际上,事情比这更复杂)只能包含指向传递给其构造函数的对象的指针,这引发了一个生存期问题。如果指针指向的对象的生存期小于该对象的生存期,则内部指针将悬空。若要防止此问题,可以通过调用(或自定义分配器)在堆上创建对象的副本。动态内存分配是人们最常提到的性能损失。std::functionstd::functionstd::functionoperator newstd::function

我最近写了一篇文章,详细介绍了如何(以及在哪里)避免支付内存分配的代价。

高效使用 Lambda 表达式和 std::function

评论

1赞 mucaho 11/6/2015
因此,这描述了构建/破坏 .boost::function 对调用性能进行了说明:“使用正确的内联编译器,函数对象的调用需要通过函数指针进行一次调用。如果调用是自由函数指针,则必须对该函数指针进行额外的调用(除非编译器具有非常强大的过程间分析)。std::function
1赞 Benjamin Barrois 3/21/2018
动态分配是否只执行一次?我的意思是,一旦初始化,它的性能是否与使用函数指针完全相同?
0赞 Alex Suo 8/16/2018
值得注意的是,如果包装的对象很小(例如,Linux 上的 std::function 不超过 16 个字节)并且打开了小对象优化,则 std::function 不会尝试进行任何堆分配。请注意,必须使用 std::cref 或 std::ref 来包装传入的参数,以避免在调用树期间复制。在这种情况下,对于没有太多参数的函数,例如 std::shared_ptr;一个简单的原语;等,则没有堆分配。如果使用简单的参数包装一些 lambda,这将特别有用。
2赞 Cassio Neri 11/1/2019
@Ruslan 可悲的是。不幸的是,DrDobbs 几年前关闭了,我不知道旧内容发生了什么。我在任何地方都找不到我的文章。我对此感到抱歉和难过:-(
2赞 Cassio Neri 3/26/2021
@MohammedNoureldin 那真是太可惜了。正如我在另一条评论中所说,DrDobbs 几年前就关闭了。有时我可以在某个地方找到旧内容,有时我找不到。我不知道我是否保留了这篇文章的副本。即使我这样做了,我也不知道我是否被允许在其他地方发布/发布它。通常,作者被要求将版权转让给出版商并失去他们的权利。(虽然多布斯博士已经死了,但他们的律师可能仍然醒着。我不记得这篇文章是否是这种情况。如果可以的话,我会尝试恢复它,但我不能保证任何事情。我对此感到非常抱歉。
2赞 Bonita Montero 9/20/2021 #5

std::function<> / std::function<> with bind( ... ) 非常快。检查一下:

#include <iostream>
#include <functional>
#include <chrono>

using namespace std;
using namespace chrono;

int main()
{
    static size_t const ROUNDS = 1'000'000'000;
    static
    auto bench = []<typename Fn>( Fn const &fn ) -> double
    {
        auto start = high_resolution_clock::now();
        fn();
        return (int64_t)duration_cast<nanoseconds>( high_resolution_clock::now() - start ).count() / (double)ROUNDS;
    };
    int i;
    static
    auto CLambda = []( int &i, int j )
    {
        i += j;
    };
    auto bCFn = [&]() -> double
    {
        void (*volatile pFnLambda)( int &i, int j ) = CLambda;
        return bench( [&]()
            {   
                for( size_t j = ROUNDS; j--; j )
                    pFnLambda( i, 2 );
            } );
    };
    auto bndObj = bind( CLambda, ref( i ), 2 );
    auto bBndObj = [&]() -> double
    {
        decltype(bndObj) *volatile pBndObj = &bndObj;
        return bench( [&]()
            {
                for( size_t j = ROUNDS; j--; j )
                    (*pBndObj)();
            } );
    };
    using fn_t = function<void()>;
    auto bFnBndObj = [&]() -> double
    {
        fn_t fnBndObj = fn_t( bndObj );
        fn_t *volatile pFnBndObj = &fnBndObj;
        return bench( [&]()
            {
                for( size_t j = ROUNDS; j--; j )
                    (*pFnBndObj)();
            } );
    };
    auto bFnBndObjCap = [&]() -> double
    {
        auto capLambda = [&i]( int j )
        {
            i += j;
        };
        fn_t fnBndObjCap = fn_t( bind( capLambda, 2 ) );
        fn_t *volatile pFnBndObjCap = &fnBndObjCap;
        return bench( [&]()
            {
                for( size_t j = ROUNDS; j--; j )
                    (*pFnBndObjCap)();
            } );
    };
    using bench_fn = function<double()>;
    static const
    struct descr_bench
    {
        char const *descr;
        bench_fn const fn;
    } dbs[] =
    {
        { "C-function",
          bench_fn( bind( bCFn ) ) },
        { "C-function in bind( ... ) with all parameters",
          bench_fn( bind( bBndObj ) ) },
        { "C-function in function<>( bind( ... ) ) with all parameters",
          bench_fn( bind( bFnBndObj ) ) },
        { "lambda capturiging first parameter in function<>( bind( lambda, 2 ) )",
          bench_fn( bind( bFnBndObjCap ) ) }
    };
    for( descr_bench const &db : dbs )
        cout << db.descr << ":" << endl,
        cout << db.fn() << endl;
}

在我的计算机上,所有呼叫都低于 2ns。