本答案旨在为现有答案集做出贡献,介绍我认为更有意义的std::function调用的运行时成本基准。
std::function机制应该被认为是提供什么的:任何可调用的实体都可以转换为适当签名的std::function。假设你有一个库,将曲面拟合到由z=f(x,y)定义的函数上,你可以编写一个接受std::function的库,并且库的用户可以轻松地将任何可调用实体转换为该函数;无论是普通函数、类实例的方法、lambda还是任何std::bind支持的东西。
与模板方法不同,这样做无需为不同情况重新编译库函数;因此,每个附加情况只需要很少的额外编译代码。这一点一直是可能的,但以前需要一些笨拙的机制,而库的用户可能需要构建一个适配器来使其正常工作。std::function自动构造所需的任何适配器,以获得所有情况的公共运行时调用接口,这是一项新的和非常强大的功能。
在我看来,这是std::function最重要的使用情况,就性能而言:我关心在std::function构造一次后多次调用它的成本,并且需要一个情况,编译器无法通过知道实际被调用的函数来优化调用(即你需要将实现隐藏在另一个源文件中以获得适当的基准)。
我进行了类似于OP的测试下面,但主要更改如下:
每种情况循环10亿次,但std::function对象仅构造一次。通过查看输出代码,我发现实际std::function调用时会调用'operator new'(可能不会在它们被优化掉时调用)。
测试分为两个文件以防止意外的优化。
我的情况是:(a)函数内联(b)函数由普通函数指针传递(c)函数作为std::function包装的兼容函数(d)函数是不兼容的函数,通过std::bind使其兼容,作为std::function包装。
我得到的结果是:
(a)(内联)1.3纳秒
所有其他情况:3.3纳秒。
情况(d)倾向于稍微慢一些,但差异(约0.05纳秒)被吸收在噪音中。
结论是std::function与使用函数指针相比在调用时具有可比的开销,即使对实际函数进行简单的“绑定”适配也是如此。内联比其他情况快2 ns,但这是预期的权衡,因为内联是唯一在运行时“硬连接”的情况。
当我在同一台机器上运行johan-lundberg的代码时,每个循环大约需要39纳秒,但在循环中有更多内容,包括std :: function的实际构造函数和析构函数,这可能相当高,因为它涉及新建和删除。
使用-O2 gcc 4.8.1,针对x86_64目标(core i5)。
请注意,该代码分为两个文件,以防止编译器在调用它们的位置扩展函数(除了打算这样做的一个例外)。
----- 第一个源文件 --------------
#include <functional>
float func_half( float x ) { return x * 0.5; }
float mul_by( float x, float scale ) { return x * scale; }
float test_stdfunc( std::function<float(float)> const & func, int nloops ) {
float x = 1.0;
float y = 0.0;
for(int i =0; i < nloops; i++ ){
y += x;
x = func(x);
}
return y;
}
float test_funcptr( float (*func)(float), int nloops ) {
float x = 1.0;
float y = 0.0;
for(int i =0; i < nloops; i++ ){
y += x;
x = func(x);
}
return y;
}
float test_inline( int nloops ) {
float x = 1.0;
float y = 0.0;
for(int i =0; i < nloops; i++ ){
y += x;
x = func_half(x);
}
return y;
}
----- 第二个源文件 -------------
#include <iostream>
#include <functional>
#include <chrono>
extern float func_half( float x );
extern float mul_by( float x, float scale );
extern float test_inline( int nloops );
extern float test_stdfunc( std::function<float(float)> const & func, int nloops );
extern float test_funcptr( float (*func)(float), int nloops );
int main() {
using namespace std::chrono;
for(int icase = 0; icase < 4; icase ++ ){
const auto tp1 = system_clock::now();
float result;
switch( icase ){
case 0:
result = test_inline( 1e9);
break;
case 1:
result = test_funcptr( func_half, 1e9);
break;
case 2:
result = test_stdfunc( func_half, 1e9);
break;
case 3:
result = test_stdfunc( std::bind( mul_by, std::placeholders::_1, 0.5), 1e9);
break;
}
const auto tp2 = high_resolution_clock::now();
const auto d = duration_cast<milliseconds>(tp2 - tp1);
std::cout << d.count() << std::endl;
std::cout << result<< std::endl;
}
return 0;
}
对于那些感兴趣的人,这里是编译器构建的适配器,使'mul_by'看起来像一个浮点数(float) - 当作为bind(mul_by,_1,0.5)创建的函数被调用时,它会被“调用”:
movq (%rdi), %rax
movsd 8(%rax), %xmm1
movq (%rax), %rdx
cvtpd2ps %xmm1, %xmm1
jmp *%rdx
(所以如果我在绑定中写入0.5f,速度可能会更快...)请注意,“x”参数到达% xmm0并保持不变。
以下是构建函数的区域中的代码,在调用test_stdfunc之前 - 通过c ++filt运行:
movl $16, %edi
movq $0, 32(%rsp)
call operator new(unsigned long) ; get 16 bytes for std::function
movsd .LC0(%rip), %xmm1 ; get 0.5
leaq 16(%rsp), %rdi ; (1st parm to test_stdfunc)
movq mul_by(float, float), (%rax) ; store &mul_by in std::function
movl $1000000000, %esi ; (2nd parm to test_stdfunc)
movsd %xmm1, 8(%rax) ; store 0.5 in std::function
movq %rax, 16(%rsp) ; save ptr to allocated mem
;; the next two ops store pointers to generated code related to the std::function.
;; the first one points to the adaptor I showed above.
movq std::_Function_handler<float (float), std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_invoke(std::_Any_data const&, float), 40(%rsp)
movq std::_Function_base::_Base_manager<std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation), 32(%rsp)
call test_stdfunc(std::function<float (float)> const&, int)
std::function
。 - Kerrek SBstd :: function
还是模板”的问题。 我认为这里的问题只是将lambda表达式包装在std :: function
中与不将其包装在其中的区别。 现在,你的问题就像在问“我应该选择苹果还是碗?” - Lightness Races in Orbit