std::function与模板的区别

183

由于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的策略,否则它们可能会使编译时间变长,
  • 至少我所知道的,没有(已知的)干净的方法来表示模板的要求(概念,任何人?),除了描述预期的函数对象类型的注释。

因此,我可以认为function可以用作传递函数对象的实际标准,在期望高性能的地方应该使用模板吗?


编辑:

我的编译器是Visual Studio 2012 不带CTP。


16
只有在需要异构可调用对象的集合(即在运行时没有进一步区分信息)时,才使用std::function - Kerrek SB
32
你正在进行错误的比较。 在这两种情况下都使用了模板 - 这不是“std :: function还是模板”的问题。 我认为这里的问题只是将lambda表达式包装在std :: function中与不将其包装在其中的区别。 现在,你的问题就像在问“我应该选择苹果还是碗?” - Lightness Races in Orbit
7
无论是1纳秒还是10纳秒,都微不足道。 - ipc
28
@ipc:虽然1000%不算什么,但正如原帖所指出的,当可扩展性对于任何实际目的变得重要时,您会开始关注它。 - Lightness Races in Orbit
21
@ipc它慢了10倍,这非常大。速度需要与基准进行比较;仅因为是纳秒而认为速度不重要是具有欺骗性的。 - Paul Manta
显示剩余16条评论
8个回答

191
一般来说,如果你面临一个涉及到 设计 的情况并且需要做出选择,使用模板。我强调了 设计 这个词,因为我认为你需要关注的是 std::function 和模板的使用情境差异,它们非常不同。
一般来说,选择使用模板只是更广泛原则的一个实例:尽可能在编译时指定尽可能多的约束条件。理由很简单:如果您可以在生成程序之前就捕捉到错误或类型不匹配,那么您就不会向客户端交付有缺陷的程序。
此外,正如您正确指出的,对模板函数的调用是静态解析的(即在编译时),因此编译器具有所有必要的信息来优化和可能内联代码(如果通过 vtable 执行调用,则这是不可能的)。
是的,模板支持并不完美,C++11 仍然缺乏对概念的支持;但是我不明白 std::function 如何在这方面帮助您。 std::function 不是模板的替代品,而是一种工具,用于无法使用模板的设计情况。
当你需要在运行时调用符合特定签名的可调用对象,但其具体类型在编译时未知时,就会出现这样的用例。这通常发生在你有一组可能不同类型的回调函数集合,但需要以统一的方式调用它们的情况下;注册的回调函数的类型和数量是根据程序状态和应用逻辑在运行时确定的。其中一些回调可以是函数对象,一些可以是普通函数,一些可以是将其他函数绑定到某些参数后的结果。
std::function和std::bind也为在C++中启用函数式编程提供了自然惯用语法,其中函数被视为对象并自然地进行了柯里化和组合以生成其他函数。虽然可以使用模板实现这种组合,但类似的设计情况通常伴随着需要在运行时确定组合可调用对象类型的用例。

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

总之,专注于设计,并尝试理解这两个结构的概念用例。如果您像您所做的那样将它们进行比较,那么您正在将它们强行置于可能不属于它们的领域中。


26
我认为,“当你有一组潜在类型不同的回调函数集合,但需要统一调用它们时,通常会出现这种情况”是重要的部分。我的经验法则是:“在存储端更倾向于使用std::function,在接口端则更倾向于使用模板Fun”。 - R. Martinho Fernandes
1
@R.MartinhoFernandes:我同意你的观点,尽管我在我的回答中试图传达的是,OP应该关注那些两个构造不重叠的用例,而不是那些可以比较并具有明显结果的用例。从技术上讲,我认为关键的设计差别始终相同:是否需要动态多态性。是的,你提到的那一点可能是最需要动态多态性的设计情况的最具代表性的部分。 - Andy Prowl
2
注意:隐藏具体类型的技术称为类型擦除(不要与托管语言中的类型擦除混淆)。它通常是通过动态多态性来实现的,但更强大(例如,unique_ptr<void>即使对于没有虚析构函数的类型也会调用适当的析构函数)。 - ecatmur
2
@ecatmur:我同意这个概念,只是在术语上我们略有不同。对我来说,动态多态性意味着“在运行时假定不同的形式”,而静态多态性则是我解释为“在编译时假定不同形式”;后者无法通过模板实现。对我来说,类型抹除在设计上是能够实现动态多态性的某种前提条件:您需要一些统一的接口以与不同类型的对象交互,并且类型抹除是一种抽象出类型特定信息的方式。 - Andy Prowl
3
在某种程度上,动态多态是概念模式,而类型擦除是一种允许实现它的技术。 - Andy Prowl
2
@Downvoter:我很想知道你在这个答案中找到了什么错误。 - Andy Prowl

95

安迪·普罗尔很好地涵盖了设计问题。当然,这非常重要,但我认为最初的问题更关注与std::function相关的性能问题。

首先,快速提一下测量技术:得到calc1的11ms根本没有意义。实际上,查看生成的汇编代码(或调试汇编代码)可以发现VS2012的优化器足够聪明,以至于意识到调用calc1的结果与迭代无关,并将调用移出循环:

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

此外,它意识到调用calc1没有任何可见效果并完全放弃了调用。因此,111毫秒是空循环运行所需的时间。(我惊讶优化器保留了循环。)因此,在循环中进行时间测量时要小心。这不像看起来那么简单。
正如指出的那样,优化器更难理解std::function并且不会将调用移出循环。因此,1241毫秒是calc2的公平测量值。
请注意,std::function能够存储不同类型的可调用对象。因此,它必须执行一些类型擦除魔术以进行存储。通常,这意味着动态内存分配(默认情况下通过调用new)。众所周知,这是一项相当昂贵的操作。
标准(20.8.11.2.1/5)鼓励实现避免为小型对象进行动态内存分配,谢天谢地,VS2012做到了(特别是对于原始代码)。
为了让您了解内存分配涉及时的速度变慢程度,我已将lambda表达式更改为捕获三个浮点数。这会使可调用对象过大,无法应用小对象优化。
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));

时间减少到约1860毫秒。

我之前写过这个问题:

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

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


我确实觉得这很有趣,想要通过使用玩具示例来证明代码速度,但编译器会将其优化掉,因为它们没有任何副作用。我认为,在没有一些真正的/生产代码的情况下,很少能够对这些测量结果打赌。 - Ghita
@ Ghita:在这个例子中,为了防止代码被优化掉,calc1可以接受一个float参数,该参数将是前一次迭代的结果。类似于 x = calc1(x, [](float arg){ return arg * 0.5f; });。此外,我们必须确保calc1使用x。但是,这还不够。我们需要创建一个副作用。例如,在测量之后,在屏幕上打印x。 尽管如此,我同意使用玩具代码进行时间测量并不能总是完美地说明实际/生产代码会发生什么。 - Cassio Neri
对我来说,基准测试在循环内部构造了std::function对象并调用了循环中的calc2。无论编译器是否会优化这一点(而且构造函数可能仅仅是存储一个vptr),我更感兴趣的是一个情况,即函数只构造一次,并传递给另一个调用它的函数进行循环。即调用开销而不是构造时间(以及调用'f'而不是calc2)。同时也想知道在循环中调用f(而非一次性调用)是否会从任何提升中受益。 - greggo
好答案。两个要点:很好的解释了std::reference_wrapper的一个有效用法(强制模板;不仅仅用于一般存储),而且有趣的是看到 VS 的优化器无法丢弃一个空循环...正如我在this GCC bug re volatile中注意到的那样。 - underscore_d

38

使用Clang编译器,这两种情况之间没有性能差异

在Linux上使用clang(3.2,trunk 166872)(-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则没有差异(见下文)。

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

Clang:

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 

所有二进制文件的计算结果都是相同的,并且所有测试都在同一台机器上执行。如果有更深入的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中存在双重->浮点数转换。删除它们不会改变Visual Studio的结论(两者都要快得多,但比率大致相同)。


9
这句话可以译为:"可以说这只是显示基准测试有误。在我看来,有趣的应用场景是调用代码从其他地方接收函数对象,因此编译器在编译调用时不知道std::function的来源。在这种情况下,编译器在调用时确切地知道std::function的组成,通过将calc2内联扩展到main中。很容易通过在单独的源文件中使calc2 'extern'来修复它。然后你就在比较苹果和橙子;calc2正在做calc1无法做到的事情。而且,循环可以在calc中(对f的多次调用)而不是在函数对象的构造函数周围。" - greggo
1
当我能够使用合适的编译器时,可以说:(a) 实际 std::function 的 ctor 调用了 'new'; (b) 当目标是匹配的实际函数时,调用本身非常简洁; (c) 在绑定的情况下,有一块代码执行适应操作,由函数对象中的代码指针选择,并从函数对象中获取数据(绑定参数); (d) 如果编译器能够看到它,'bound' 函数可能会被内联到该适配器中。 - greggo
使用所描述的设置添加了新答案。 - greggo
3
顺便说一句,基准测试结果并没有错误。这个问题(“std::function vs template”)只在同一编译单元的范围内有效。如果将函数移到另一个单元中,模板就不再适用了,因此也就无法进行比较了。 - rustyx

14

不同并不相同。

它更慢是因为它可以做模板无法做到的事情。特别地,它可以让你从同一段代码中调用任何可调用给定参数类型并且返回类型可转换为给定返回类型的函数。

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的两次调用中。它持有两个不同的函数。

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


2
只是想指出,当执行“fun=f2”时,“fun”对象最终会指向一个隐藏函数,该函数将int转换为double,调用f2,然后将double结果转换回int。(在实际示例中,“f2”可能会内联到该函数中)。如果将std :: bind分配给fun,则“fun”对象最终可能包含要用于绑定参数的值。为了支持这种灵活性,“fun”的分配(或初始化)可能涉及分配/释放内存,并且可能比实际调用开销更长。 - greggo

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

7
本答案旨在为现有答案集做出贡献,介绍我认为更有意义的std::function调用的运行时成本基准。
std::function机制应该被认为是提供什么的:任何可调用的实体都可以转换为适当签名的std::function。假设你有一个库,将曲面拟合到由z=f(x,y)定义的函数上,你可以编写一个接受std::function的库,并且库的用户可以轻松地将任何可调用实体转换为该函数;无论是普通函数、类实例的方法、lambda还是任何std::bind支持的东西。
与模板方法不同,这样做无需为不同情况重新编译库函数;因此,每个附加情况只需要很少的额外编译代码。这一点一直是可能的,但以前需要一些笨拙的机制,而库的用户可能需要构建一个适配器来使其正常工作。std::function自动构造所需的任何适配器,以获得所有情况的公共运行时调用接口,这是一项新的和非常强大的功能。
在我看来,这是std::function最重要的使用情况,就性能而言:我关心在std::function构造一次后多次调用它的成本,并且需要一个情况,编译器无法通过知道实际被调用的函数来优化调用(即你需要将实现隐藏在另一个源文件中以获得适当的基准)。
我进行了类似于OP的测试下面,但主要更改如下:
每种情况循环10亿次,但std::function对象仅构造一次。通过查看输出代码,我发现实际std::function调用时会调用'operator new'(可能不会在它们被优化掉时调用)。
测试分为两个文件以防止意外的优化。
我的情况是:(a)函数内联(b)函数由普通函数指针传递(c)函数作为std::function包装的兼容函数(d)函数是不兼容的函数,通过std::bind使其兼容,作为std::function包装。
我得到的结果是:
(a)(内联)1.3纳秒
所有其他情况:3.3纳秒。
情况(d)倾向于稍微慢一些,但差异(约0.05纳秒)被吸收在噪音中。
结论是std::function与使用函数指针相比在调用时具有可比的开销,即使对实际函数进行简单的“绑定”适配也是如此。内联比其他情况快2 ns,但这是预期的权衡,因为内联是唯一在运行时“硬连接”的情况。
当我在同一台机器上运行johan-lundberg的代码时,每个循环大约需要39纳秒,但在循环中有更多内容,包括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) - 当作为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)

1
使用clang 3.4.1 x64编译的结果为:(a) 1.0,(b) 0.95,(c) 2.0,(d) 5.0。 - rustyx

6

如果你在C++20中使用模板而不是std::function,那么你可以使用可变模板编写自己的概念(concept)(灵感来源于Hendrik Niemeyer关于C++20概念的演讲:https://www.youtube.com/watch?v=N_kPd2OK1L8):

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<Ret, Args...> F>,其中 Ret 为返回值,Args... 为可变输入参数。例如:functor<double,int> F 如下所示:
template <functor<double,int> F>
auto CalculateSomething(F&& f, int const arg) {
  return f(arg)*f(arg);
}

需要一个函数对象作为模板参数,该函数对象必须重载()运算符,并具有返回值类型为double和单个输入参数类型为int。类似地,functor<double>将是一个具有double返回类型且不带任何输入参数的函数对象。

在此处尝试!

您还可以将其用于可变参数函数,例如:

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

在此处进行尝试!


4

我发现你的结果非常有趣,所以我进行了一些探究,了解到正在发生什么。首先,像其他人所说的那样,如果计算结果不会影响程序状态,编译器会优化掉它。其次,给回调函数提供一个恒定的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编译,并得到了330ms的calc1时间和2702的calc2时间。因此,使用模板的速度比较快,这个数字让我有些怀疑,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();
}

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

  • 模板:330毫秒
  • std::function:2702毫秒
  • 全局std::function:330毫秒

因此,我的结论是,std::function与模板函数对象的原始速度几乎相同。但是,它使优化器的工作更加困难。


1
整个重点是将一个函数对象作为参数传递。你的 calc3 情况没有意义;现在 calc3 被硬编码为调用 f2。当然,这可以进行优化。 - rustyx
确实,这就是我想要展示的。Calc3等效于模板,在这种情况下,它就像模板一样成为了编译时构造。 - Joshua Ritterman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接