【问题标题】:std::function vs templatestd::function 与模板
【发布时间】:2013-01-18 15:16:33
【问题描述】:

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

在我看来,模板显然存在问题:

  • 它们必须作为标头提供,在将库作为封闭代码发布时,您可能不希望这样做,
  • 除非引入类似extern template 的策略,否则它们可能会使编译时间更长,
  • 没有(至少我知道)简洁的方式来表示模板的需求(概念,有人吗?),除非有描述预期什么样的函子的注释。

因此我可以假设functions 可以用作传递函子的事实上的标准,并且在期望高性能的地方应该使用模板吗?


编辑:

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

【问题讨论】:

  • 当且仅当您真正需要可调用对象的异构集合时使用std::function(即在运行时没有进一步的区分信息可用)。
  • 你在比较错误的东西。在这两种情况下都使用模板 - 它不是“std::function 或模板”。我认为这里的问题只是在std::function 中包装了一个lambda,而不是在std::function 中包装了一个lambda。目前,您的问题就像是在问“我应该更喜欢苹果还是碗?”
  • 不管是1ns还是10ns,都不算什么。
  • @ipc: 1000% 不是没有。正如 OP 所指出的那样,当可扩展性出于任何实际目的而涉及时,您就会开始关心它。
  • @ipc 慢了 10 倍,这是巨大的。速度需要与基线进行比较;认为它无关紧要只是因为它是纳秒是欺骗性的。

标签: c++ templates c++11 std-function


【解决方案1】:

一般来说,如果您面临设计让您有选择余地的情况,请使用模板。我强调了设计这个词,因为我认为你需要关注的是std::function的用例和模板之间的区别,它们是完全不同的。

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

此外,正如您正确指出的那样,对模板函数的调用是静态解析的(即在编译时),因此编译器具有优化并可能内联代码的所有必要信息(如果调用是通过 vtable 执行)。

是的,模板支持确实不完美,C++11还缺乏对概念的支持;但是,我看不出std::function 在这方面会如何拯救你。 std::function 不是模板的替代品,而是用于无法使用模板的设计情况的工具。

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

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

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

总而言之,专注于设计,并尝试了解这两种结构的概念用例是什么。如果你以你的方式将他们进行比较,你就是在强迫他们进入一个他们可能不属于的领域。

【讨论】:

  • 我认为“这通常是当你有一组可能不同类型的回调,但你需要统一调用时;”是重要的一点。我的经验法则是:“在存储端首选std::function,在接口上首选模板Fun”。
  • 注意:隐藏具体类型的技术称为类型擦除(不要与托管语言中的类型擦除混淆)。它通常根据动态多态性来实现,但功能更强大(例如,unique_ptr&lt;void&gt; 即使对于没有虚拟析构函数的类型也会调用适当的析构函数)。
  • @ecatmur:我同意实质内容,尽管我们在术语上略有不同。动态多态性对我来说意味着“在运行时采用不同的形式”,而不是静态多态性,我将其解释为“在编译时采用不同的形式”;后者无法通过模板实现。对我来说,类型擦除在设计上是一种能够实现动态多态性的先决条件:你需要一些统一的接口来与不同类型的对象交互,而类型擦除是一种抽象类型的方法——具体信息。
  • @ecatmur:所以从某种意义上说,动态多态是概念模式,而类型擦除是一种允许实现它的技术。
  • @Downvoter:我很想知道你在这个答案中发现了什么错误。
【解决方案2】:

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 时遇到了更多麻烦,并且不会将调用移出循环。所以 1241 毫秒对于 calc2 来说是一个公平的衡量标准。

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

标准 (20.8.11.2.1/5) 鼓励实现以避免为小对象分配动态内存,幸运的是,VS2012 可以做到这一点(特别是对于原始代码)。

为了了解在涉及内存分配时它会变慢多少,我更改了 lambda 表达式以捕获三个floats。这使得可调用对象太大而无法应用小对象优化:

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

对于这个版本,时间大约是 16000 毫秒(原始代码为 1241 毫秒)。

最后,请注意 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));

时间减少到大约 1860 毫秒。

我不久前写过:

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

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

【讨论】:

  • 我确实觉得这很有趣,我想使用玩具示例来证明代码速度,这些玩具示例会被编译器优化掉,因为它们没有任何副作用。我想说,如果没有一些真实/生产代码,很少有人可以对这些测量结果下注。
  • @Ghita:在这个例子中,为了防止代码被优化掉,calc1 可以接受一个 float 参数,这将是前一次迭代的结果。像x = calc1(x, [](float arg){ return arg * 0.5f; }); 这样的东西。此外,我们必须确保calc1 使用x。但是,这还不够。我们需要创造一个副作用。例如,测量后,在屏幕上打印x。尽管如此,我同意使用玩具代码进行计时测量并不总是能完美地表明真实/生产代码会发生什么。
  • 在我看来,基准测试也在循环内构造 std::function 对象,并在循环中调用 calc2。无论编译器可能会或可能不会优化它,(并且构造函数可以像存储 vptr 一样简单),我会对函数构造一次并传递给另一个调用的函数的情况更感兴趣它在一个循环中。 IE。调用开销而不是构造时间(以及调用“f”而不是调用 calc2)。如果在循环中(在 calc2 中)而不是一次调用 f 将受益于任何提升,也会感兴趣。
  • 很好的答案。两件事:std::reference_wrapper 有效使用的好例子(强制模板;它不只是用于一般存储),有趣的是看到 VS 的优化器未能丢弃空循环......正如我注意到的 this GCC bug re volatile
【解决方案3】:

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

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

-我会在帖子结束时回来clang。但首先,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! Visual Studio 的差异接近 2 倍,而 clang 则没有差异(见下文)。

此外,由于乘法速度很快,因此谈论减速因素通常没那么有趣。一个更有趣的问题是,你的函数有多小,这些调用是实际程序中的瓶颈吗?

叮当声:

当我在示例代码的 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 的结论(两者都快得多,但比例大致相同)。

【讨论】:

  • 这可以说只是表明基准是错误的。恕我直言,有趣的用例是调用代码从其他地方接收函数对象,因此编译器在编译调用时不知道 std::function 的来源。在这里,编译器通过将 calc2 inline 展开到 main 中,在调用它时准确地知道 std::function 的组成。通过在 sep 中使 calc2 'extern' 轻松修复。源文件。然后,您正在比较苹果和橙子; calc2 正在做一些 calc1 做不到的事情。而且,循环可能在 calc 内部(对 f 的许多调用);不在函数对象的 ctor 周围。
  • 什么时候可以找到合适的编译器。现在可以说 (a) 实际 std::function 的 ctor 调用 'new'; (b) 当目标是匹配的实际函数时,调用本身非常精简; (c) 在绑定的情况下,有一段代码进行适配,由函数 obj 中的代码 ptr 选择,并从函数 obj 中获取数据(绑定参数) (d) “绑定”函数可能如果编译器可以看到,则内联到该适配器中。
  • 使用描述的设置添加了新答案。
  • BTW 基准没有错,问题(“std::function vs template”)只在同一个编译单元的范围内有效。如果你把函数移到另一个单元,模板不再可能,所以没有什么可比较的。
【解决方案4】:

不同不一样。

它的速度较慢,因为它可以做模板不能做的事情。特别是,它允许您调用任何函数,这些函数可以使用给定的参数类型调用,并且其返回类型可以从相同的代码转换为给定的返回类型。。 p>

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;
}

请注意,same 函数对象 fun 被传递给对 eval 的两个调用。它拥有两个不同的功能。

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

【讨论】:

  • 只想指出,当 'fun=f2' 完成时,'fun' 对象最终指向一个隐藏函数,该函数将 int 转换为 double,调用 f2,并将 double 结果转换回来到 int.(在实际示例中,'f2' 可以内联到该函数中)。如果将 std::bind 分配给 fun,则“fun”对象最终可能包含要用于绑定参数的值。为了支持这种灵活性,分配给“fun”(或 init of)可能涉及分配/取消分配内存,并且它可能需要比实际调用开销更长的时间。
【解决方案5】:

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

【讨论】:

    【解决方案6】:

    此答案旨在为现有答案集贡献我认为对 std::function 调用的运行时成本更有意义的基准。

    应该识别 std::function 机制提供的内容:任何可调用实体都可以转换为具有适当签名的 std::function。假设您有一个库,该库将曲面拟合到由 z = f(x,y) 定义的函数,您可以编写它以接受 std::function&lt;double(double,double)&gt;,并且库的用户可以轻松地将任何可调用实体转换为该函数;无论是普通函数、类实例的方法、lambda,还是 std::bind 支持的任何东西。

    与模板方法不同,这无需针对不同情况重新编译库函数即可工作;因此,对于每个额外的情况,几乎不需要额外的编译代码。实现这一点一直是可能的,但它过去需要一些笨拙的机制,并且库的用户可能需要围绕他们的函数构建一个适配器才能使其工作。 std::function 会自动构造所需的任何适配器,以获得所有情况下的通用 runtime 调用接口,这是一个非常强大的新功能。

    在我看来,就性能而言,这是 std::function 最重要的用例:我对在构造一次 std::function 后多次调用它的成本感兴趣,并且它必须是编译器无法通过知道实际调用的函数来优化调用的情况(即,您需要将实现隐藏在另一个源文件中以获得正确的基准)。

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

    1. 每个 case 循环 10 亿次,但 std::function 对象只构造一次。通过查看输出代码,我发现在构造实际的 std::function 调用时调用了“operator new”(可能不是在优化它们时)。
    2. 测试被分成两个文件以防止不希望的优化
    3. 我的情况是:(a) 函数是内联的 (b) 函数由普通函数指针传递 (c) 函数是包装为 std::function 的兼容函数 (d) 函数是与 a 兼容的不兼容函数std::bind,包装为 std::function

    我得到的结果是:

    • case (a) (inline) 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)
    

    【讨论】:

    • 使用 clang 3.4.1 x64 的结果是:(a) 1.0, (b) 0.95, (c) 2.0, (d) 5.0。
    【解决方案7】:

    我发现您的结果非常有趣,因此我进行了一些挖掘以了解发生了什么。首先,正如许多其他人所说,在没有计算结果影响的情况下,编译器只会优化程序的状态。其次,有一个常数 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 的时间为 330 毫秒,而 calc2 的时间为 2702。所以使用模板快了 8 倍,这个数字在我看来是可疑的,8 的幂的速度通常表明编译器已经向量化了一些东西。当我查看为模板版本生成的代码时,它显然是矢量化的

    .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 可能会改变,因此不能向量化。

    这导致我尝试其他方法,看看是否可以让编译器在 std::function 版本上执行相同的优化。我没有传入函数,而是将 std::function 作为全局变量,并调用它。

    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();
    }
    

    在这个版本中,我们看到编译器现在以相同的方式对代码进行了矢量化,我得到了相同的基准测试结果。

    • 模板:330ms
    • std::function : 2702ms
    • 全局 std::function: 330ms

    所以我的结论是 std::function 与模板仿函数的原始速度几乎相同。然而,它使优化器的工作变得更加困难。

    【讨论】:

    • 重点是将仿函数作为参数传递。您的calc3 案例毫无意义; calc3 现在被硬编码为调用 f2。当然可以优化。
    • 确实,这就是我想要展示的。 calc3 相当于模板,在这种情况下,它实际上是一个编译时构造,就像模板一样。
    【解决方案8】:

    如果您在 C++20 中使用 模板 而不是 std::function,您实际上可以使用可变参数模板编写自己的 概念为它(inspired by Hendrik Niemeyer's talk about C++20 concepts):

    template<class Func, typename Ret, typename... Args>
    concept functor = std::regular_invocable<Func, Args...> && 
                      std::same_as<std::invoke_result_t<Func, Args...>, Ret>;
    

    然后您可以将其用作functor&lt;Ret, Args...&gt; F&gt;,其中Ret 是返回值,Args... 是可变参数输入参数。例如。 functor&lt;double,int&gt; F

    template <functor<double,int> F>
    auto CalculateSomething(F&& f, int const arg) {
      return f(arg)*f(arg);
    }
    

    需要一个函子作为模板参数,它必须重载() 运算符,并具有double 返回值和int 类型的单个输入参数。类似地,functor&lt;double&gt; 将是一个具有 double 返回类型的函子,它不接受任何输入参数。

    Try it here!

    您也可以将它与可变参数函数一起使用,例如

    template <typename... Args, functor<double, Args...> F>
    auto CalculateSomething(F&& f, Args... args) {
      return f(args...)*f(args...);
    }
    

    Try it here!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-01-03
      • 2015-02-20
      相关资源
      最近更新 更多