该答案旨在为现有答案集做出贡献,我认为这是对 std::function 调用的运行时成本更有意义的基准。
应该识别 std::function 机制提供的内容:任何可调用实体都可以转换为具有适当签名的 std::function。假设您有一个库,该库将曲面拟合到由 z = f(x,y) 定义的函数,您可以将其编写为接受 a std::function<double(double,double)>
,并且库的用户可以轻松地将任何可调用实体转换为该函数;无论是普通函数、类实例的方法、lambda,还是 std::bind 支持的任何东西。
与模板方法不同,这无需针对不同情况重新编译库函数即可工作;因此,对于每个额外的情况,几乎不需要额外的编译代码。实现这一点一直是可能的,但它过去需要一些笨拙的机制,并且库的用户可能需要围绕他们的函数构建一个适配器才能使其工作。std::function 自动构造所需的任何适配器,以获得所有情况下的公共运行时调用接口,这是一个非常强大的新功能。
在我看来,就性能而言,这是 std::function 最重要的用例:我对在构造一次 std::function 后多次调用它的成本感兴趣,它需要是编译器无法通过知道实际调用的函数来优化调用的情况(即,您需要将实现隐藏在另一个源文件中以获得正确的基准)。
我在下面进行了测试,类似于 OP;但主要变化是:
- 每个 case 循环 10 亿次,但 std::function 对象只构造一次。通过查看输出代码,我发现在构造实际的 std::function 调用时调用了“operator new”(可能不是在优化它们时)。
- 测试被分成两个文件以防止不希望的优化
- 我的情况是: (a) 函数是内联的 (b) 函数由普通函数指针传递 (c) 函数是包装为 std::function 的兼容函数 (d) 函数是与 std:: 兼容的不兼容函数绑定,包装为 std::function
我得到的结果是:
案例(a)(内联)1.3 nsec
所有其他情况:3.3 纳秒。
情况 (d) 往往会稍微慢一些,但差异(大约 0.05 纳秒)被噪声吸收了。
结论是 std::function 与使用函数指针的开销(在调用时)相当,即使对实际函数进行了简单的“绑定”调整。内联比其他方法快 2 ns,但这是一个预期的权衡,因为内联是唯一在运行时“硬连线”的情况。
当我在同一台机器上运行 johan-lundberg 的代码时,我看到每个循环大约 39 纳秒,但那里的循环还有很多,包括 std::function 的实际构造函数和析构函数,这可能相当高因为它涉及新建和删除。
-O2 gcc 4.8.1,到 x86_64 目标(核心 i5)。
请注意,代码被分成两个文件,以防止编译器在调用它们的地方扩展函数(除了打算这样做的一种情况)。
----- 第一个源文件 --------------
#include <functional>
// simple funct
float func_half( float x ) { return x * 0.5; }
// func we can bind
float mul_by( float x, float scale ) { return x * scale; }
//
// func to call another func a zillion times.
//
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;
}
// same thing with a function pointer
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;
}
// same thing with inline function
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(float) 而构建的适配器 - 当调用创建为 bind(mul_by,_1,0.5) 的函数时,这是“调用”的:
movq (%rdi), %rax ; get the std::func data
movsd 8(%rax), %xmm1 ; get the bound value (0.5)
movq (%rax), %rdx ; get the function to call (mul_by)
cvtpd2ps %xmm1, %xmm1 ; convert 0.5 to 0.5f
jmp *%rdx ; jump to the func
(所以如果我在绑定中写了 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)