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

93

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


38
相较于哪种替代方案,会导致性能下降? - Fred Nurk
5
你需要给出比这更具体的信息,user408141。 - Lightness Races in Orbit
7
实际上,这是一个非常差的问题。 - sbi
2
哦,抱歉,我太笨拙了!:D - user408141
2
我不认为这是一个差劲的问题。因为我将调用一个普通的自由函数,并想知道它与std::function的区别在哪里。由于您可以使用自由函数、成员函数、仿函数、std::bind的结果、lamdas来填充std::function,因此我将在性能时间和生成的代码方面与它们中的每一个进行比较。 - SJHowe
显示剩余2条评论
5个回答

110

在使用std::function时,确实存在性能问题需要考虑。 std::function 的主要优势是其类型擦除机制,但这不是免费的,我们可能(但不一定)需要为此付出代价。

std::function 是一个模板类,用于封装可调用类型。然而,它不是根据可调用类型本身进行参数化的,而仅仅是基于其返回和参数类型。 可调用类型仅在构造函数时才知道,因此,std::function 无法有一个预先声明的成员来保存给定给其构造函数的对象的副本。

粗略地说(实际上,情况比这更复杂),std::function 只能持有指向传递给其构造函数的对象的指针,这会引发生命周期问题。如果指针指向的对象的生命周期小于 std::function 对象的生命周期,则内部指针将变得悬空。为了防止这个问题,std::function 可能通过调用 operator new(或自定义分配器)在堆上复制对象。人们最常提到的性能惩罚是动态内存分配。

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

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


1
这里描述了构造/析构std::function的开销。boost::function关于调用性能的说明如下:“使用适当的内联编译器,函数对象的调用只需通过一个函数指针进行一次调用。如果调用是由于自由函数指针引起的,则还必须对该函数指针进行其他调用(除非编译器具有非常强大的过程间分析)。” - mucaho
1
动态分配只执行一次吗?我的意思是,一旦初始化,它是否像使用函数指针一样执行? - Benjamin Barrois
2
@Ruslan 很遗憾。不幸的是,DrDobbs 几年前关闭了,我不知道旧内容发生了什么。我无法在任何地方找到我的文章。对此我感到非常抱歉和难过 :-( - Cassio Neri
@Ruslan 我可以建议这篇文章,它引用了我的方法作为类似的例子。我的方法是这篇文章所称的“解决方案3”。 - Cassio Neri
2
@MohammedNoureldin 真遗憾。正如我在另一条评论中所说,DrDobbs 几年前关闭了。有时我可以在某个地方找到旧内容,有时则不行。我不知道是否保存了这篇文章的副本。即使我保存了,我也不知道是否被允许在其他地方发布/张贴它。通常,作者需要将版权让给出版商并失去他们的权利。(尽管 DrDobbs 已经死了,他们的律师仍然可能醒着。)我记不清这篇文章是否是这种情况。如果可以的话,我会尝试恢复它,但我不能保证任何事情。真的很抱歉。 - Cassio Neri
显示剩余4条评论

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

这并不能决定是否使用boost函数。根据程序的要求,性能下降可能是可以接受的。通常情况下,程序的某些部分并不关键。即使如此,这也可能是可以接受的。这只有您自己才能确定。
至于标准库版本,标准只定义了一个接口。完全由个别实现来实现其功能。我想应该会使用类似于boost function的实现。

15
首先,随着函数的内部处理量增加,开销会变得更小;工作量越高,开销就越小。
其次,与虚函数相比,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而不是虚拟调用来改进,请尝试使用它们。就我个人而言,我非常喜欢不强制客户端使用我的类的接口和继承的想法。


1
std::function可以很容易地通过虚函数实现。然而,大多数实现似乎使用函数指针来模板化函数和一个void *指针。间接性实际上是相同的。 - Xeo
3
@Xeo:没错。但是验证比信仰更好 :) 当您不使用优化时,相同的测试显示与std::function相比有1:3的差异,因此这个测试并不完全没有道理。 - Sebastian Mach
2
使用G++ 4.8.2编译时,我始终得到2.9、3.3和3.3秒的结果。如果我添加“-flto”,它们都变成了3.3。我的猜测是GCC实际上尝试优化std::function(类似于使用虚函数得到的-flto),但这些优化实际上会产生负面影响。 - MWB
4
使用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-devirtualize);2.0、2.3、2.3(-O2 -fno-devirtualize)。因此,似乎在较新的g++版本中,虚函数优化已经得到了足够的改进,这不再是一种非最优解。 - Paul Brannan
4
使用 g++ 6.3.0 编译 impl.cpp 和 main.cpp,参数为 -std=gnu++14、-O3、-flto 和 -march=native,然后运行生成的 a.out 文件。输出三个数字分别为 1073741824,使用虚函数用时 1.97619 秒,使用 std::function 用时 6.86855 秒,使用带绑定的 std::function 用时 6.86847 秒。 - Alexandre Pereira Nunes
3
g++ 7.4.0 在 Ubuntu 18.04 (AMD 2400G) 上的运行结果为:g++ --std=c++17 -O3 impl.cc main.cc && ./a.out。虚函数(virtual)用时 1.38742 秒,std::function 用时 1.44681 秒,带绑定的 std::function 用时 1.39367 秒。 - Andrej Kesely

14

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

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

确实需要有东西来进行比较,不能仅仅说它“减少了开销”而不与使用其他传递函数的替代方式进行比较。如果完全可以不使用它,则从一开始就不需要它。


4
如果实现使用小缓冲区优化将函数对象存储在std::function实例中,并且传递的可调用对象大小适合SBO,则即使绑定参数也可能不会产生动态分配。 - underscore_d

2

使用bind(...)的std::function<> / std::function<>非常快。请查看以下内容:

#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;
}

在我的电脑上,所有的调用都在2纳秒以下。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接