std :: function vs模板

感谢C ++ 11,我们收到了函数包装器的std::function系列。 不幸的是,我一直听到这些新增加的坏消息。 最受欢迎的是它们非常缓慢。 我testing了它,他们真的与模板比较吸。

 #include <iostream> #include <functional> #include <string> #include <chrono> template <typename F> float calc1(F f) { return -1.0f * f(3.3f) + 666.0f; } float calc2(std::function<float(float)> f) { return -1.0f * f(3.3f) + 666.0f; } int main() { using namespace std::chrono; const auto tp1 = system_clock::now(); for (int i = 0; i < 1e8; ++i) { calc1([](float arg){ return arg * 0.5f; }); } const auto tp2 = high_resolution_clock::now(); const auto d = duration_cast<milliseconds>(tp2 - tp1); std::cout << d.count() << std::endl; return 0; } 

111毫秒vs 1241毫秒。 我认为这是因为模板可以很好地内联,而function通过虚拟调用覆盖内部。

当我看到它们时,显然模板有问题:

  • 他们必须被提供作为标题,这是不是你可能不希望作为一个封闭的代码释放你的图书馆时,
  • 除非引入extern template策略,否则可能会使编译时间更长,
  • 没有(至less我知道)代表一个模板的要求(概念,任何人?)的简洁方法,禁止一个描述什么样的函子的评论。

我是否可以假定function s可以作为传递函数的事实上的标准,并且在那些期望高性能的地方使用模板呢?


编辑:

我的编译器是没有 CTP的Visual Studio 2012。

一般来说,如果您面临的devise情况可供您select,请使用模板 。 我强调了“ devise”这个词,因为我认为你需要关注的是std::function和模板的用例之间的区别。

一般来说,模板的select只是一个更广泛的原则的实例: 尽可能在编译时指定尽可能多的约束条件 。 理由很简单:即使在生成程序之前,如果可以发现错误或types不匹配,则不会将错误程序发送给客户。

而且,正如您正确指出的那样,对模板函数的调用是静态parsing的(即在编译时),所以编译器拥有所有必要的信息来优化和可能内联代码(如果调用是通过虚函数表)。

是的,模板支持并不完美,C ++ 11仍然缺乏对概念的支持。 但是,我不明白如何std::function将在这方面拯救你。 std::function不是模板的替代品,而是模板无法使用的devise情况的工具。

当需要在运行时通过调用符合特定签名的可调用对象来parsing调用,但在编译时其具体types未知时,会出现这种用例。 当您有可能不同types的callback集合,但是您需要统一调用时,通常会出现这种情况。 注册callback的types和数量是根据程序的状态和应用程序逻辑在运行时确定的。 这些callback中的一些可能是函数,有些可能是简单的函数,有些可能是其他函数绑定到某些参数的结果。

std::functionstd::bind也提供了一个自然的习惯用于在C ++中启用函数式编程 ,其中函数被视为对象,并自然地被压缩和组合以产生其他函数。 尽pipe这种组合也可以通过模板来实现,但是类似的devise情况通常与需要在运行时确定组合的可调用对象的types的用例一起出现。

最后,还有其他一些情况, std::function是不可避免的,例如,如果你想写recursionlambdas ; 然而,这些限制更多地受技术限制的限制,而不是我认为的概念上的限制。

总结一下, 着重于devise,并试图理解这两个构造的概念用例是什么。 如果你把它们与你所做的相比,那么你迫使它们进入一个他们不属于的竞技场。

Andy Prowl很好地涵盖了devise问题。 这当然是非常重要的,但我相信原来的问题涉及更多与std::function相关的性能问题。

首先,对测量技术做一个简短的评论: calc1获得的11ms毫无意义。 事实上,查看生成的程序集(或者debugging程序集代码),可以看到VS2012的优化器足够聪明,可以实现调用calc1的结果与迭代无关,并将调用移出循环:

 for (int i = 0; i < 1e8; ++i) { } calc1([](float arg){ return arg * 0.5f; }); 

此外,它意识到,调用calc1没有可见的效果,并完全放弃呼叫。 因此111ms是空循环运行的时间。 (我很惊讶,优化器保持循环。)所以,要小心循环中的时间测量。 这并不像看起来那么简单。

正如已经指出的那样,优化器在理解std::function方面有更多的麻烦,并且不会将调用移出循环。 所以1241ms是calc2的公平测量。

请注意, std::function能够存储不同types的可调用对象。 因此,它必须执行一些types擦除魔术的存储。 通常,这意味着dynamic内存分配(默认情况下通过调用new )。 众所周知,这是一个相当昂贵的操作。

标准(20.8.11.2.1 / 5)包含了实现,以避免为VS2012(尤其是原始代码)所做的小对象的dynamic内存分配。

为了了解在涉及内存分配时可以得到多less速度,我已经改变了lambdaexpression式来捕获三个float 。 这使得可调用对象太大而无法应用小对象优化:

 float a, b, c; // never mind the values // ... calc2([a,b,c](float arg){ return arg * 0.5f; }); 

对于这个版本,时间大约是16000ms(与原始代码的1241ms相比)。

最后请注意,lambda的生命周期包含了std::function的生命周期。 在这种情况下,不是存储lambda的副本, std::function可以存储一个“引用”给它。 “引用”我的意思是一个std::reference_wrapper ,它很容易通过函数std::refstd::cref构build。 更确切地说,通过使用:

 auto func = [a,b,c](float arg){ return arg * 0.5f; }; calc2(std::cref(func)); 

时间减less到大约1860ms。

我刚才写到:

http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059

正如我在文章中所说的那样,由于对C ++ 11的支持不佳,这些论点并不适用于VS2010。 在写这篇文章的时候,只有VS2012的testing版本可用,但是对C ++ 11的支持对于这个问题已经足够了。

与Clang,两者之间没有性能差异

使用clang(3.2,trunk 166872)(Linux上的-O2), 两种情况下的二进制文件实际上是相同的

我会在post结尾回来。 但首先,gcc 4.7.2:

已经有很多的见解了,但是我想指出的是,calc1和calc2的计算结果是不一样的,因为内联等。比较所有结果的总和:

 float result=0; for (int i = 0; i < 1e8; ++i) { result+=calc2([](float arg){ return arg * 0.5f; }); } 

与calc2成为

 1.71799e+10, time spent 0.14 sec 

而与calc1它成为

 6.6435e+10, time spent 5.772 sec 

速度差异约为40倍,数值约为4倍。 第一个是比OP发布的(使用visual studio)更大的差异。 实际上打印出来的值也是一个好主意,以防止编译器删除没有可见结果的代码(as-if规则)。 卡西奥内里已经在他的回答中这样说过了。 注意结果有多不同 – 比较执行不同计算的代码的速度因子时,应该小心。

另外,公平地说,比较重复计算f(3.3)的各种方法可能不是那么有趣。 如果input是恒定的,它不应该在一个循环中。 (优化器很容易注意到)

如果我将一个用户提供的值参数添加到calc1和2,则calc1和calc2之间的速度因子将从40降低到5的因子! 与视觉工作室的差异接近2倍,与铿锵有没有区别(见下文)。

另外,由于乘法运算速度很快,所以谈论减速因素往往不是那么有趣。 一个更有意思的问题是,你的function有多小,这是否是真正的程序中的瓶颈呢?

铛:

叮当(我用3.2)实际上产生相同的二进制文件,当我翻转calc1和calc2之间的示例代码(下面发布)。 在问题中发布的原始示例都是相同的,但是完全没有时间(如上所述,循环只是完全删除)。 用我修改的例子,用-O2:

执行的秒数(最好的3):

 clang: calc1: 1.4 seconds clang: calc2: 1.4 seconds (identical binary) gcc 4.7.2: calc1: 1.1 seconds gcc 4.7.2: calc2: 6.0 seconds VS2012 CTPNov calc1: 0.8 seconds VS2012 CTPNov calc2: 2.0 seconds VS2015 (14.0.23.107) calc1: 1.1 seconds VS2015 (14.0.23.107) calc2: 1.5 seconds MinGW (4.7.2) calc1: 0.9 seconds MinGW (4.7.2) calc2: 20.5 seconds 

所有的二进制文件的计算结果是相同的,所有的testing都在同一台机器上执行。 如果有更深刻的叮当声或VS知识的人可以评论可能已经完成的优化,那将是有趣的。

我修改过的testing代码:

 #include <functional> #include <chrono> #include <iostream> template <typename F> float calc1(F f, float x) { return 1.0f + 0.002*x+f(x*1.223) ; } float calc2(std::function<float(float)> f,float x) { return 1.0f + 0.002*x+f(x*1.223) ; } int main() { using namespace std::chrono; const auto tp1 = high_resolution_clock::now(); float result=0; for (int i = 0; i < 1e8; ++i) { result=calc1([](float arg){ return arg * 0.5f; },result); } 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; } 

更新:

新增vs2015。 我还注意到在calc1,calc2中有double-> float转换。 删除它们并不会改变视觉工作室的结论(两者速度都要快很多,但比例大致相同)。

不同的是不一样的。

这是因为它做的事情,模板不能做的更慢。 特别是,它可以让你调用任何可以用给定的参数types调用的函数,并且它的返回types可以从相同的代码转换为给定的返回types。

 void eval(const std::function<int(int)>& f) { std::cout << f(3); } int f1(int i) { return i; } float f2(double d) { return d; } int main() { std::function<int(int)> fun(f1); eval(fun); fun = f2; eval(fun); return 0; } 

请注意, 相同的函数对象fun ,正被传递给eval两个调用。 它有两个不同的function。

如果你不需要这样做,那么你不应该使用std::function

你已经有了一些很好的答案,所以我不会反驳它们,简而言之,将std :: function与模板比较就像比较虚函数和函数。 你永远不应该“偏好”虚拟function,而是在适合问题的时候使用虚拟function,将决策从编译时间转移到运行时间。 这个想法是,而不是必须使用定制的解决scheme(如跳转表)解决问题,你使用的东西,让编译器更好的机会为你优化。 如果您使用标准解决scheme,它也可以帮助其他程序员。

这个答案的目的是为现有的答案做出贡献,我认为这是对std :: function调用的运行时成本更有意义的基准。

std :: function机制应该被识别为它所提供的:任何可调用的实体都可以被转换成适当签名的std :: function。 假设你有一个适合于由z = f(x,y)定义的函数的表面的库,你可以写它来接受一个std::function<double(double,double)> ,并且库的用户可以轻松地将任何可调用实体转换为 无论是普通函数,类实例的方法还是lambda,或者std :: bind支持的任何东西。

与模板方法不同,这不需要为不同的情况重新编译库函数; 因此,对于每个附加的情况,只需要less量额外的编译代码。 实现这一切始终是可能的,但过去需要一些尴尬的机制,而库的用户可能需要围绕它们的函数构造一个适配器来使其工作。 std :: function自动构造所需的适配器,以获得所有情况下的公共运行时调用接口,这是一个新的强大的function。

在我看来,就性能而言,这是std :: function最重要的用例:我感兴趣的是在构造一次std :: function之后多次调用std :: function的代价,当编译器无法知道实际调用的函数(即,您需要将实现隐藏在另一个源文件中以获得适当的基准)时,无法优化调用的情况。

我做了下面的testing,类似于OP的; 但主要的变化是:

  1. 每个case循环10亿次,但std :: function对象只构造一次。 我发现通过查看输出代码,当构build实际的std :: function调用(可能不是当他们被优化出来)时,调用“operator new”被调用。
  2. testing分为两个文件,以防止不必要的优化
  3. 我的情况是:(a)内联函数(b)函数是通过普通函数传递的(c)函数是一个兼容函数,包装为std :: function(d)函数是一个不兼容的函数,与std ::绑定,包装为std ::函数

我得到的结果是:

  • 情况(a)(内联)1.3纳秒

  • 所有其他情况:3.3纳秒。

情况(d)倾向于稍微慢一些,但噪声中吸收了差异(约0.05纳秒)。

结论是std :: function在使用一个函数指针的时候可以比较开销(在调用的时候),即使对实际函数有一个简单的“绑定”调整。 内联比其他内联快2ns,但这是一个预期的折衷,因为内联是唯一在运行时“硬连线”的情况。

当我在同一台机器上运行johan-lundberg的代码时,我看到每个循环约有39nsec,但循环中还有很多,包括std :: function的实际构造函数和析构函数,这可能相当高因为它涉及到一个新的和删除。

-O2 gcc 4.8.1,到x86_64目标(core 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)而构build的适配器 – 当调用创build为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的forms出现,并保持在那里。

在调用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) 

我发现你的结果非常有趣,所以我做了一些挖掘,以了解发生了什么。 首先,许多人已经说了计算效果的结果,程序的状态,编译器将只是优化了这一点。 其次,作为callback武器的常数3.3我怀疑还会有其他的优化。 考虑到这一点,我改变了你的基准代码。

 template <typename F> float calc1(F f, float i) { return -1.0f * f(i) + 666.0f; } float calc2(std::function<float(float)> f, float i) { return -1.0f * f(i) + 666.0f; } int main() { const auto tp1 = system_clock::now(); for (int i = 0; i < 1e8; ++i) { t += calc2([&](float arg){ return arg * 0.5f + t; }, i); } const auto tp2 = high_resolution_clock::now(); } 

考虑到我用gcc 4.8 -O3编译的代码的这个改变,得到calc1的330ms和calc2的2702的时间。 所以使用模板的速度快了8倍,这个数字对我来说是嫌疑的,8的幂的速度常常表示编译器已经向量化了一些东西。 当我查看模板版本的生成代码时,它显然是vectoreized

 .L34: cvtsi2ss %edx, %xmm0 addl $1, %edx movaps %xmm3, %xmm5 mulss %xmm4, %xmm0 addss %xmm1, %xmm0 subss %xmm0, %xmm5 movaps %xmm5, %xmm0 addss %xmm1, %xmm0 cvtsi2sd %edx, %xmm1 ucomisd %xmm1, %xmm2 ja .L37 movss %xmm0, 16(%rsp) 

作为std :: function版本不是。 这对我来说是有道理的,因为使用这个模板,编译器确信函数在整个循环中永远不会改变,但是传入的std :: function可能会改变,因此不能被vector化。

这导致我去尝试别的东西,看看我是否可以让编译器在std :: function版本上执行相同的优化。 而不是传入一个函数,我做一个std ::函数作为一个全局variables,并有这个叫。

 float calc3(float i) { return -1.0f * f2(i) + 666.0f; } std::function<float(float)> f2 = [](float arg){ return arg * 0.5f; }; int main() { const auto tp1 = system_clock::now(); for (int i = 0; i < 1e8; ++i) { t += calc3([&](float arg){ return arg * 0.5f + t; }, i); } const auto tp2 = high_resolution_clock::now(); } 

通过这个版本,我们可以看到编译器现在已经以相同的方式对代码进行了vector化处理,并得到了相同的基准testing结果。

  • 模板:330ms
  • std :: function:2702ms
  • 全球标准::function:330毫秒

所以我的结论是std :: function与模板函子的原始速度几乎是一样的。 但是,这使得优化器的工作更加困难。