为了账号安全,请及时绑定邮箱和手机立即绑定

std :: function vs template

std :: function vs template

C++
慕村9548890 2019-11-05 14:41:55
感谢C ++ 11,我们获得了std::function函子包装器系列。不幸的是,我一直只听到关于这些新功能的坏消息。最受欢迎的是,它们运行缓慢。我测试了一下,与模板相比,它们确实很烂。#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毫秒和1241毫秒。我想这是因为模板可以很好地内联,而function封面通过虚拟呼叫的内部。显然,模板在我看来有其问题:他们的头是不是你可能不希望释放你的库作为一个封闭的代码时做的提供,除非extern template引入类似政策,否则它们可能会使编译时间更长。没有(至少在我的面前)代表要求的清洁方式(概念,任何人吗?)模板,扎描述什么样的函子有望评论。因此,我可以假定functions可以用作传递函子的事实上的标准,并且应该在期望使用高性能模板的地方使用吗?
查看完整描述

3 回答

?
开满天机

TA贡献1786条经验 获得超13个赞

一般来说,如果你正面临着一个设计情况,给你一个选择,使用模板。我之所以强调“ 设计 ”一词,是因为我认为您需要关注的是的用例std::function和模板之间的区别,两者之间有很大的不同。


通常,模板的选择只是更广泛原则的一个实例:尝试在编译时指定尽可能多的约束。理由很简单:如果你能赶上一个错误,或者类型不匹配,产生程序,甚至之前,你将不会运送一个错误的程序给客户。


此外,当你正确地指出,调用静态解析模板函数(即在编译时),所以编译器具有所有必要的信息,以优化和可能的内联的代码(如果呼叫是通过执行这将是不可能的vtable)。


是的,确实模板支持不是完美的,并且C ++ 11仍然缺少对概念的支持;这是对的。但是,我看不出如何std::function在这方面为您省钱。std::function不能替代模板,而是用于无法使用模板的设计情况的工具。


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


std::function并且std::bind还提供了一个自然的习惯用法,用于启用C ++中的函数式编程,其中将函数视为对象,并自然进行咖喱化和组合以生成其他函数。尽管也可以通过模板来实现这种组合,但是类似的设计情况通常伴随用例,这些用例需要在运行时确定组合的可调用对象的类型。


最后,还有其他一些std::function不可避免的情况,例如,如果您要编写递归lambda;但是,这些限制更多是由技术限制决定的,而不是我认为是概念上的区别。


综上所述,专注于设计并尝试了解这两种构造的概念性用例是什么。如果按照您做的方式将它们进行比较,则会迫使他们进入他们可能不属于的竞技场。


查看完整回答
反对 回复 2019-11-05
?
饮歌长啸

TA贡献1951条经验 获得超3个赞

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

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


-我将在帖子结尾再说一遍。但首先,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)相比,差异要大得多。实际上,最终输出值也是防止编译器删除无可见结果的代码的一个好主意(按规则)。卡西欧·内里(Cassio Neri)已经在回答中说了这一点。请注意结果有何不同-比较执行不同计算的代码的速度因子时应格外小心。


同样,公平地说,比较重复计算f(3.3)的各种方式可能并不那么有趣。如果输入是恒定的,则不应循环。(优化程序很容易注意到)


如果我将用户提供的value参数添加到calc1和2,则calc1和calc2之间的速度因数将从40降为5!使用Visual Studio时,差异接近2倍,而使用lang声时则没有差异(请参见下文)。


另外,由于乘法运算很快,谈论减速的因素通常并不那么有趣。一个更有趣的问题是,您的函数有多小?这些调用是否成为实际程序中的瓶颈?


铛:

当我在示例代码的calc1和calc2之间翻转时,Clang(我使用3.2)实际上产生了相同的二进制文件(如下所述)。在问题中张贴原始示例的情况下,两者也是相同的,但根本不需要时间(如上所述,循环已被完全删除)。在我修改的示例中,使用-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 

所有二进制文件的计算结果都相同,并且所有测试都在同一台计算机上执行。如果有更深的clang或VS知识的人可以评论可能已经进行了哪些优化,那将很有趣。


我修改的测试代码:

#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转换。删除它们并不会改变Visual Studio的结论(两者都快很多,但是比率大致相同)。


查看完整回答
反对 回复 2019-11-05
  • 3 回答
  • 0 关注
  • 499 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信