当前位置: 首页 > 知识库问答 >
问题:

std::函数与模板

燕元明
2023-03-14

多亏了C11,我们收到了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毫秒。我认为这是因为模板可以很好地内联,而函数通过虚拟调用覆盖内部。

显然,在我看来,模板也有其问题:

  • 它们必须以头的形式提供,这不是您在以封闭代码形式发布库时可能不希望做的事情,

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

编辑:

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

共有3个答案

夏季萌
2023-03-14

使用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发布的差异要大得多(使用可视化工作室)。实际上打印出值a end也是一个好主意,可以防止编译器删除没有可见结果的代码(就像规则一样)。Cassio Neri已经在他的答案中说过了。请注意结果有多么不同——在比较执行不同计算的代码的速度因素时应该小心。

此外,公平地说,比较各种重复计算f(3.3)的方法可能没有那么有趣。如果输入为常量,则不应处于循环中。(优化器很容易注意到)

如果我在calc1和calc2中添加一个用户提供的值参数,那么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。我还注意到有双-

卫彭亮
2023-03-14

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.21/5)包含实现,以避免小对象的动态存储分配,谢天谢地,VS2012做到了这一点(特别是对于原始代码)。

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

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的“引用”,而不是存储lambda的副本。“reference”指的是一个std::reference_包装,它可以通过函数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

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

宫晟
2023-03-14

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

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

此外,正如您正确指出的那样,对模板函数的调用是静态解析的(即在编译时),因此编译器拥有所有必要的信息来优化和可能内联代码(如果调用是通过vtablehtml" target="_blank">执行的,这是不可能的)。

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

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

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

最后,还有一些情况下,std::function是不可避免的,例如,如果您想编写递归lambda;然而,我认为,这些限制更多地是由技术限制而非概念差异决定的。

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

 类似资料:
  • 我目前有一个,但是为了灵活性,我希望能够分配一个lambda表达式,将作为映射中的值返回。 所以我创建了这个模板类: 并像这样使用它: IntelliSense提供了更多信息: 多个操作符“=”匹配这些操作数:function“valueorfunction::operator=(const std::function&other)[with T=std::wstring]”function“va

  • 假设我们有我的代码的简化版本: 我试图将lambda函数作为参数传递给函数,但只有当我将lambda显式分配给特定的类型时,它才起作用。这是可行的: 上面的例子是可行的,但我想实现下面的例子,因为审美的原因,不知道这是否可能: 我唯一的要求是应该接受带有模板化返回类型的lambda和函数,以及带有特定类型的输入参数,例如。

  • 本文向大家介绍c++11 符号修饰与函数签名、函数指针、匿名函数、仿函数、std::function与std::bind,包括了c++11 符号修饰与函数签名、函数指针、匿名函数、仿函数、std::function与std::bind的使用技巧和注意事项,需要的朋友参考一下 一、符号修饰与函数签名 1、符号修饰 编译器将c++源代码编译成目标文件时,用函数签名的信息对函数名进行改编,形成修饰名。G

  • 假设有一个模板函数 接受任意数量的参数。给定最后一个参数始终是 ,我如何实现下面显示的 模板,以便 包含此 的参数? 例如,如果像这样调用,应该是: 我的第一个想法是: 但这不会编译: 问题一: 为什么这不能编译?为什么模板参数演绎失败? 最终,我想出了这个解决方案: 这里,是,即中的最后一种类型。然后,的临时值被传递到,其中。这行得通,但在我看来很难看。 问题二: 我想知道如果没有两个函数和的临

  • 我试图编写一个通用的obj工厂,使用可变模板调用各种类的构造函数。代码如下: 在大多数例子中,变量arg总是这样写“Args” 错误:没有可行的转换从'__bind( 在移除“ 但我不知道为什么?

  • 我发现了和类型推导的以下行为,这对我来说是意想不到的: 中的两行都会导致错误: 没有函数模板“stdfunc_test”的实例与参数列表匹配 尝试在Visual Studio 2015中编译时。 为什么类型演绎不从函数类型中演绎模板类型,有没有变通方法?