0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Lambda和函数指针的性能对比

CPP开发者 来源:CppMore 2024-10-21 13:43 次阅读

以下文章来源于CppMore,作者里缪


引言

很多时候,选择单一,事情做来不会有多少阻力,选择太多 ,倒是举棋难定了。

C++ 复杂性的一方面就体现在选择太多,对于同一种需求,可能存在数十种不同的方式都能够解决,此时每种方式的优劣便是学习的难点。

std::function,函数指针, std::bind, Lambda 就是这样的一些组件,使用频率不低,差异细微,许多人不清楚何时使用何种方式,常常误用,致使程序性能出现瓶颈。

本文全面地对比了这些组件间的细微差异,并评估不同方式的性能优劣,提出使用建议及一些实践经验。

首先要明确谁与谁对比,理清可替代对象,这样对比起来才有意义。

std::function 的对比对象是函数指针,它们主要是为了支持函数的延迟调用;std::bind的对比对象是Lambda 和std::bind_front,主要是为了支持参数化绑定。

本文会全面对比这些方式的运行时间、编译时间、内存占用和指令读取总数。

旧事

函数若是不想被立即执行,在 C 及 C++11 以前存在许多方式,函数指针是最普遍的一种方式。看个例子:

voidfoo(intx){
std::cout<< "Function called with " << x << '
';
}

void bar(void (*pf)(int), int value) {
    pf(value); // delayed invocation
}

int main() {
    bar(foo, 10);
}

通过函数指针实现了函数的延迟调用,这在回调函数、事件处理、惰性计算等场景下被广泛使用。C++11 之前,提供了仿函数来代替函数指针,于是上述示例可以等价写成:

structfunctor{
voidoperator()(intx)const{
std::cout<< "Function called with " << x << '
';
    }
};

void bar(const functor& func, int value) {
    func(value); // delayed invocation
}

int main() {
    bar(functor(), 10);
}

相比函数指针,仿函数具有更好的灵活性和安全性,它可以持有状态,可以有成员函数和成员变量,并且更加容易被编译器优化。而函数指针涉及间接调用,编译器不会对其进行内联优化,还有可能出现类型转换错误。

由于函数指针无法持有状态,C 里面一般会增加一个状态参数来捕获状态,例如:

typedefint(*add_pf)(void*,int);

intadd_with_state(void*state,intx){
intincrement=*(int*)state;
returnx+increment;
}

intbar(add_pffunc,void*state,intvalue){
returnfunc(state,value);//delayedinvocation
}

intmain(){
intincrement=5;
add_pfadd=add_with_state;
returnbar(add,&increment,10);//return15
}

仿函数则稍微简单一点,等价写法为:

classadd_functor{
intincrement;
public:
add_functor(intinc):increment(inc){}
intoperator()(intx)const{
returnx+increment;
}
};

intbar(constadd_functor&func,intvalue){
returnfunc(value);//delayedinvocation
}

intmain(){
add_functoradd(5);
returnbar(add,10);//return15
}

相较之下,仿函数捕获状态方便很多,语法也更加清晰简洁。

早期 C++ 还提供 std::bind1st 和 std::bind2nd 来绑定函数,以下是一个例子:

intadd(intx,inty){
returnx+y;
}

intmain(){
autobound_func=std::bind1st(std::ptr_fun(add),5);
returnbound_func(10);//return15
}

不过如今都已废弃,std::bind1st 被 std::bind 代码,std::ptr_fun 被 std::function 代替。

旧事且过,来看新的方法。

std::function vs.Function pointer

std::function 是 C++11 对于可调用体的高度抽象组件,不仅能够持有普通函数和成员函数,还能够持有仿函数、Lambda 和其他类型的可调用体。

一个组件的抽象层次越高,考虑的越周全,额外的工作也就越多,开销也会更大。

下面通过一个简单的例子,对比一下 std::function 和函数指针的生成代码。

////////////////////////////////
//functionpointer
intadd(intx,inty){
returnx+y;
}

intbar(int(*func)(int,int),intx,inty){
returnfunc(x,y);
}

intmain(){
returnbar(add,5,10);//return15
}

////////////////////////////////
//std::function
intadd(intx,inty){
returnx+y;
}

intbar(std::functionfunc,intx,inty){
returnfunc(x,y);
}

intmain(){
returnbar(add,5,10);//return15
}

在 GCC 13.2 最高级别的优化下,函数指针( https://godbolt.org/z/vno8WaYTK )生成的汇编代码只有 11 行,而std::function ( https://godbolt.org/z/W71bWo3qj )生成的却有 60 行,差异巨大。

实际 Benchmarks 一下,测试代码为:

intadd(intx,inty){
returnx+y;
}

intbar_function_ptr(int(*func)(int,int),intx,inty){
returnfunc(x,y);
}

intbar_function(std::functionfunc,intx,inty){
returnfunc(x,y);
}

staticvoidfunction_ptr_bench(benchmark::State&state){
for(auto_:state){
intresult=bar_function_ptr(add,5,10);
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(function_ptr_bench);

staticvoidfunction_bench(benchmark::State&state){
for(auto_:state){
intresult=bar_function(add,5,10);
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(function_bench);

结果不出所料,std::function 的运行开销要远远大于函数指针。

123cbc9790783c01d8e305021ac1c411.png
func-ptr-vs-function-benchmarks

既然函数指针效率这么高,那还要 std::function 干嘛?

除了旧事一节提到的关于函数指针的缺点,还有一个很大的不同在于一致性,std::function 能持有普通函数、成员函数、仿函数、Lambda 等等可调用体,灵活性突出,函数指针可没有这个能力,是以适用性更低。

请注意,尽管本节的对比结果表明函数指针效率更高,但却并非是说推荐使用函数指针。

std::bindvs. std::bind_front vs. Lambda vs.Function pointer

std::bind 和 Lambda 都是 C++11 入的标准,然而,它们的功能重叠性很高,Lambda 几乎可以完全替代 std::bind。

std::bind_front则是C++20 用来替代std::bind的新特性,其灵活性和便捷性更好。

本篇的核心是对比性能,关于它们之间区别的文章已指不胜屈,只是缺少性能分析方面的文章,故这里不会赘述已有内容。

先来测试一下基本性能,测试例子如下:

#include
#include

intadd(intx,inty){
returnx+y;
}

typedefint(*pf)(int,int);

staticvoidfunc_ptr(benchmark::State&state){
intval=42;
pfadd_func=add;

for(auto_:state){
intresult=add_func(val,10);
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(func_ptr);

staticvoidlambda(benchmark::State&state){
intval=42;
constautolam=[val](inty){
returnval+y;
};

for(auto_:state){
intresult=lam(10);
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(lambda);

staticvoidbind(benchmark::State&state){
intval=42;
constautobind=std::bind(add,val,std::_1);
for(auto_:state){
intresult=bind(10);
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(bind);

staticvoidbind_front(benchmark::State&state){
intval=42;
constautobind=std::bind_front(add,val);
for(auto_:state){
intresult=bind(10);
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(bind_front);

编译器 GCC 13.2,不开优化,对比结果如下图所示。

c9f32a3076f4c3257799b0b0a69b4312.png

可见,在设计上,Lambda 并不会比函数指针更慢,而 std::bind 却将近慢了二十倍,std::bind_front 则比 std::bind 效率高许多,只慢了近十倍。

注意这是在未开优化的情况下,事实上,如今的编译器优化能力很强,示例相对过于简单,优化后的效率是一样的。但若是换成早期的编译器,或是更加复杂的例子,效率和未开优化的情况基本是一致的。

可以换一种编译器,并降低其版本来观察不同优化级别下的表现。编译器切换为 Clang 10.0。

O0 级别优化,对比结果如下图所示。

b28e86df41c23b1aef74b01731e43e32.png

O1 级别优化,对比结果如下图所示。

05c807e14c8eb36af308d7e5628d9359.png

O2 级别优化效果,结果如下图所示。

091bc55131c21a01681ca55cbd872666.png

到这个优化级别,四种方式的性能已经持平。

虽说不同编译器的数值有所差异,但对比结果的整体趋势基本一致。这个结果表明 std::bind 的确是性能杀手,应该优先使用 Lambda 或 std::bind_front 代替。

Lambda vs.Functor

Lambda 就是一个可以携带状态的函数。

其实现是一个含有 operator() 重载的匿名类,捕获的参数作为匿名类的数据成员直接初始化。Lambda 使用时调用的便是这个重载的 operator(),返回的类型就是匿名类的类型,称为 closure type。

Lambda 就是为简化仿函数(即函数对象)而来,无需在其他地方创建一个仿函数,直接原地构造。因此,它们的性能基本是一致的。

加上以下测试代码,和前面的 Lambda 代码进行对比,验证结果。

structFunctor{
intx;
autooperator()(inty)const{
returnx+y;
}
};

staticvoidfunctor(benchmark::State&state){
intval=42;
Functorfunctor(val);
for(auto_:state){
intresult=functor(10);
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(functor);

对比结果如下图所示。

wKgZomcV6m6AKO9uAAA2xfGnYUw589.png

结果表明结论正确。

Lambda vs std::function

Lambda 和 std::function得分两种情况进行对比,一种是无需存储可调用体,一种是需要存储可调用体。

先看第一种情况,测试代码为:

intcallable_with_lambda(autofunc){
returnfunc(1,2);
}

intcallable_with_funtional(std::functionfunc){
returnfunc(1,2);
}

staticvoidpass_callable_with_lambda(benchmark::State&state){
for(auto_:state){
intresult=callable_with_lambda([](inta,intb){
returna+b;
});
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(pass_callable_with_lambda);

staticvoidpass_callable_with_funtional(benchmark::State&state){
for(auto_:state){
intresult=callable_with_funtional([](inta,intb){
returna+b;
});
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(pass_callable_with_funtional);

测试环境依旧是 GCC 13.2,不开优化。对比结果如下图。

1b488fecca6f4cf8d1a9974d387e00e8.png

由此可知,Lambda 的开销要比 std::function 小很多,应该优先使用泛型 Lambda 传递可调用体。

再来看第二种情况,这种情况需要存储可调用体,然而 Lambda 为 Closure type,只有使用 auto 或 decltype()才能推导出具体类型,它是无法存储的。

此时,你只能使用std::function 或函数指针。具体使用哪种方式,便需要在性能、便捷性、灵活性等方面作出取舍。若是倾向于最大的便捷性和灵活性,前者是更好的选择;若是追求最大化性能,函数指针则是更好的方式。但需注意,若是选择函数指针,调用者将无法再使用 Lambda 和std::bind等常见方式传递参数。

Lambda vs.Function Pointer

对比内容前文已涉,本节作为补充。

Lambda 是可以隐式转换为函数指针的,只需将形式写成 +[]{}(注意不能捕获状态)。其性能与函数指针无异,这也是 Lambda 被广泛使用的原因之一。Lambda 也可以携带状态,并和 std::invocable Concept 配合起来接受可调用对象,集灵活性和高性能于一身。

函数指针涉及间接调用,无法被编译器优化,是以既无法内联,也无法重新排序。它可能指向不同的函数,编译器无法优化这些调用的具体细节,必须按照特定的调用约定进行处理。而 Lambda 在编译时就可知道具体实现,编译器可以直接生成高效的调用代码,无需遵循通用的调用约定,这将带来巨大的优化空间。

此外,只要满足 constexpr function 的条件,Lambda 会隐式 constexpr,因此可以在编译期评估。

编译时间、内存占用、指令读取:std::bind vs. std::bind_front vs. Lambda

前文只是对比了这些方式在运行时间方面的性能,本节再对比编译时间和内存占用。

对比示例,代码如下:

//bind.cpp
//////////////////////////////
#include
#include

intadd(intx,inty){
//std::cout<< "x: " << x << " y:" << y << '
';
    return x + y;
}

int main() {
    int val = 42;
    const auto fun =  std::bind(add, val, std::_1);

    for (int i = 0; i < 1000000; ++i) {
        fun(i);
    }
}

// bind_front.cpp
//////////////////////////////
#include
#include

intadd(intx,inty){
//std::cout<< "x: " << x << " y:" << y << '
';
    return x + y;
}

int main() {
    int val = 42;
    const auto fun =  std::bind_front(add, val);

    for (int i = 0; i < 1000000; ++i) {
        fun(i);
    }
}

// lambda.cpp
//////////////////////////////
#include
#include

intadd(intx,inty){
//std::cout<< "x: " << x << " y:" << y << '
';
    return x + y;
}

int main() {
    int val = 42;
    const auto lam = [val](int y) {
        return add(val, y);
    };

    for (int i = 0; i < 1000000; ++i) {
        lam(i);
    }
}

首先,来看编译时间和内存占用情况。如下图所示。

17f33838b2278ae952baaba414b82f21.png

可以看到,Lambda 消耗的时间最短,只有 1.27 秒,Bind 消耗的时间最多,1.34 秒;Lambda 的最大常驻内存大小为 96640KB,Bind Front 为 98436KB,而 Bind 是 100100KB。

其次,再来对比一下它们的指令读取情况。如下图。

c96bfc5bc2e602e883be27ad2fa3f5fa.png

其中,Lambda 运行期间指令总共读取了32,265,989 次,Bind 是 390,268,192 次,而 Bind Front 是 262,267,908 次。可见,Lambda 比其他两种方式的指令读取次数少了一个数量级,Bind Front 较 Bind 也减少了非常多次。

最后,不难得出,无论是在运行时间,还是编译时间、内存占用和指令读取方面,Lambda 的性能都是最好的,其次是 Bind Front,最后是 Bind。

总结

本文全面对比了 Lambda、std::bind、std::bind_front、std::function 和函数指针的性能,针对不同场景分析不同方式的优劣,以能够根据场景灵活选择适当的实现方式。

Lambda 的性能(运行时间、编译时间、内存占用、指令读取总数)最好,和函数指针基本持平,其次是 std::bind_front,最后是 std::bind。std::bind 是失败的设计,任何时候,都要优先使用 Lambda 或 std::bind_front。

当不需要具体的可调用对象类型时,使用模板和 Lambda 的方式要优于 std::function,其保留了灵活性和高性能;当需要具体的类型时,std::function 能够提供最大的灵活性和便捷性,此时若想追求最大化性能,可考虑函数指针(将失去所有灵活性)。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • C++
    C++
    +关注

    关注

    22

    文章

    2109

    浏览量

    73663
  • 代码
    +关注

    关注

    30

    文章

    4789

    浏览量

    68638
  • 函数指针
    +关注

    关注

    2

    文章

    56

    浏览量

    3801
  • Lambda
    +关注

    关注

    0

    文章

    29

    浏览量

    9880

原文标题:Lambda, bind(front), std::function, Function Pointer Benchmarks

文章出处:【微信号:CPP开发者,微信公众号:CPP开发者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    函数指针指针函数的概念

    不少朋友会混淆“函数指针”和“指针函数”这两个概念,本文详细介绍一下。
    发表于 03-09 10:49 1213次阅读

    Nanopi系列板子资源性能对比

    Nanopi系列板子资源性能对比对比性能 选择适合你的板子
    发表于 08-05 14:21

    SparkRDMA基于BigDataBench的性能对比测试

    SparkRDMA基于BigDataBench 性能对比测试
    发表于 05-04 13:16

    Linux下AWTK与Qt的性能对比

    为了比较直观的看到AWTK的基本性能,我们对产品开发者比较关心GUI的一些参数做了测试,如界面刷新帧数、启动时间等。让我们从参数上直观了解Linux下AWTK与Qt的性能对比
    发表于 10-29 08:26

    lambda函数基础

    lambda函数基础lambda与def
    发表于 12-29 06:22

    Arm Cortex-A35性能对比分析

    Arm Cortex-A35性能对比
    发表于 01-19 07:44

    arduino和stm32性能对比究竟谁更厉害?

    一些DIY和各种小项目?arduino和stm32性能对比究竟谁更厉害呢?我们一起来讨论一下。比较两者之前首先我们来了解下arduino和stm32的特点:Arduino:Arduino UNO-DFRobot商城1. Arduino更倾向于创意,它弱化了具体的硬件的操作,它的
    发表于 01-24 07:14

    函数指针指针函数定义

    函数指针指针函数,C语言学习中最容易混淆的一些概念,好好学习吧
    发表于 01-11 16:44 0次下载

    C语言指针函数函数指针详细介绍

    C语言指针函数函数指针详细介绍。。。。。。。
    发表于 03-04 15:27 5次下载

    c语言函数指针定义,指针函数函数指针的区别

     往往,我们一提到指针函数函数指针的时候,就有很多人弄不懂。下面就由小编详细为大家介绍C语言中函数指针
    发表于 11-16 15:18 3628次阅读

    理解函数指针函数指针数组、函数指针数组的指针

    理解函数指针函数指针数组、函数指针数组的指针
    的头像 发表于 06-29 15:38 1.5w次阅读
    理解<b class='flag-5'>函数</b><b class='flag-5'>指针</b>、<b class='flag-5'>函数</b><b class='flag-5'>指针</b>数组、<b class='flag-5'>函数</b><b class='flag-5'>指针</b>数组的<b class='flag-5'>指针</b>

    什么是Lambda函数

    今天来给大家推荐一个 Python 当中超级好用的内置函数,那便是 lambda 方法,本篇教程大致和大家分享 什么是 lambda 函数 lamb
    的头像 发表于 10-17 11:27 1199次阅读

    函数指针指针函数是不是一个东西?

    函数指针的本质是指针,就跟整型指针、字符指针一样,函数指针
    的头像 发表于 01-03 16:35 536次阅读
    <b class='flag-5'>函数</b><b class='flag-5'>指针</b>和<b class='flag-5'>指针</b><b class='flag-5'>函数</b>是不是一个东西?

    面试常考+1:函数指针指针函数、数组指针指针数组

    在嵌入式开发领域,函数指针指针函数、数组指针指针数组是一些非常重要但又容易混淆的概念。理解它
    的头像 发表于 08-10 08:11 870次阅读
    面试常考+1:<b class='flag-5'>函数</b><b class='flag-5'>指针</b>与<b class='flag-5'>指针</b><b class='flag-5'>函数</b>、数组<b class='flag-5'>指针</b>与<b class='flag-5'>指针</b>数组

    亚马逊云科技推出Amazon Lambda SnapStart功能

    亚马逊云科技推出Amazon Lambda SnapStart,大幅提升Java Lambda函数启动性能   北京,2024年10月29日 —— 亚马逊云科技近日宣布,与光环新
    的头像 发表于 10-30 10:59 261次阅读