std :: function vs模板

感谢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毫秒vs 1241毫秒。 我认为这是因为模板可以很好地内联,而function通过虚拟调用覆盖内部。

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

  • 它们必须作为标题提供,这不是你在将你的库作为一个封闭代码发布时不希望做的事情,
  • 除非引入extern template式策略,否则它们可能会使编译时间更长,
  • 没有(至少我知道)代表模板的需求(概念,任何人?)的干净方式,请注明描述了期望的函子的类型。
  • 我是否可以假设function s可以用作传递函数的事实上的标准,并且在期望高性能的地方应该使用模板?


    编辑:

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


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

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

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

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

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

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

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

    总结一下, 着重于设计并尝试理解这两个构造的概念用例。 如果你按照你所做的方式来比较它们,你就会迫使它们进入一个他们可能不属于的竞技场。


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

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

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

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

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

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

    标准(20.8.11.2.1 / 5)包含了实现,以避免为小型对象的动态内存分配,谢天谢地,VS2012确实(尤其是原始代码)。

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

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

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

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

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

    时间减少到大约1860ms。

    我刚才写到:

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

    正如我在文章中所说的,由于对C ++ 11的支持不佳,这些论据并不完全适用于VS2010。 在撰写本文时,只有VS2012的beta版本可用,但它对C ++ 11的支持已经足够用于此事。


    随着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)更大的差异。 实际上,将值打印出来也是一个好主意,以防止编译器删除没有可见结果的代码(as-if规则)。 Cassio Neri在他的回答中已经说过了。 注意结果有多不同 - 在比较执行不同计算的代码的速度因子时,应该小心。

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

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

    另外,由于乘法运算速度很快,因此讨论减速因素往往不是那么有趣。 一个更有趣的问题是,你的功能有多小,并且这些调用是真正程序中的瓶颈?

    铛:

    Clang(我用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 
    

    所有二进制文件的计算结果是相同的,所有的测试都在同一台机器上执行。 如果有更深刻的叮当声或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转换。 去除它们并不会改变视觉工作室的结论(两者速度都快了很多,但比例大致相同)。

    链接地址: http://www.djcxy.com/p/90145.html

    上一篇: std::function vs template

    下一篇: if predicated on std::is