0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Lambda和函數(shù)指針的性能對比

CPP開發(fā)者 ? 來源:CppMore ? 2024-10-21 13:43 ? 次閱讀

以下文章來源于CppMore,作者里繆


引言

很多時候,選擇單一,事情做來不會有多少阻力,選擇太多 ,倒是舉棋難定了。

C++ 復雜性的一方面就體現(xiàn)在選擇太多,對于同一種需求,可能存在數(shù)十種不同的方式都能夠解決,此時每種方式的優(yōu)劣便是學習的難點。

std::function,函數(shù)指針, std::bind, Lambda 就是這樣的一些組件,使用頻率不低,差異細微,許多人不清楚何時使用何種方式,常常誤用,致使程序性能出現(xiàn)瓶頸。

本文全面地對比了這些組件間的細微差異,并評估不同方式的性能優(yōu)劣,提出使用建議及一些實踐經(jīng)驗。

首先要明確誰與誰對比,理清可替代對象,這樣對比起來才有意義。

std::function 的對比對象是函數(shù)指針,它們主要是為了支持函數(shù)的延遲調(diào)用;std::bind的對比對象是Lambda 和std::bind_front,主要是為了支持參數(shù)化綁定。

本文會全面對比這些方式的運行時間、編譯時間、內(nèi)存占用和指令讀取總數(shù)。

舊事

函數(shù)若是不想被立即執(zhí)行,在 C 及 C++11 以前存在許多方式,函數(shù)指針是最普遍的一種方式??磦€例子:

voidfoo(intx){
std::cout<

通過函數(shù)指針實現(xiàn)了函數(shù)的延遲調(diào)用,這在回調(diào)函數(shù)、事件處理、惰性計算等場景下被廣泛使用。C++11 之前,提供了仿函數(shù)來代替函數(shù)指針,于是上述示例可以等價寫成:

structfunctor{
voidoperator()(intx)const{
std::cout<

相比函數(shù)指針,仿函數(shù)具有更好的靈活性和安全性,它可以持有狀態(tài),可以有成員函數(shù)和成員變量,并且更加容易被編譯器優(yōu)化。而函數(shù)指針涉及間接調(diào)用,編譯器不會對其進行內(nèi)聯(lián)優(yōu)化,還有可能出現(xiàn)類型轉(zhuǎn)換錯誤。

由于函數(shù)指針無法持有狀態(tài),C 里面一般會增加一個狀態(tài)參數(shù)來捕獲狀態(tài),例如:

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
}

仿函數(shù)則稍微簡單一點,等價寫法為:

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
}

相較之下,仿函數(shù)捕獲狀態(tài)方便很多,語法也更加清晰簡潔。

早期 C++ 還提供 std::bind1st 和 std::bind2nd 來綁定函數(shù),以下是一個例子:

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 對于可調(diào)用體的高度抽象組件,不僅能夠持有普通函數(shù)和成員函數(shù),還能夠持有仿函數(shù)、Lambda 和其他類型的可調(diào)用體。

一個組件的抽象層次越高,考慮的越周全,額外的工作也就越多,開銷也會更大。

下面通過一個簡單的例子,對比一下 std::function 和函數(shù)指針的生成代碼。

////////////////////////////////
//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 最高級別的優(yōu)化下,函數(shù)指針( 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);

結(jié)果不出所料,std::function 的運行開銷要遠遠大于函數(shù)指針。

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

既然函數(shù)指針效率這么高,那還要 std::function 干嘛?

除了舊事一節(jié)提到的關(guān)于函數(shù)指針的缺點,還有一個很大的不同在于一致性,std::function 能持有普通函數(shù)、成員函數(shù)、仿函數(shù)、Lambda 等等可調(diào)用體,靈活性突出,函數(shù)指針可沒有這個能力,是以適用性更低。

請注意,盡管本節(jié)的對比結(jié)果表明函數(shù)指針效率更高,但卻并非是說推薦使用函數(shù)指針。

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的新特性,其靈活性和便捷性更好。

本篇的核心是對比性能,關(guān)于它們之間區(qū)別的文章已指不勝屈,只是缺少性能分析方面的文章,故這里不會贅述已有內(nèi)容。

先來測試一下基本性能,測試例子如下:

#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,不開優(yōu)化,對比結(jié)果如下圖所示。

c9f32a3076f4c3257799b0b0a69b4312.png

可見,在設計上,Lambda 并不會比函數(shù)指針更慢,而 std::bind 卻將近慢了二十倍,std::bind_front 則比 std::bind 效率高許多,只慢了近十倍。

注意這是在未開優(yōu)化的情況下,事實上,如今的編譯器優(yōu)化能力很強,示例相對過于簡單,優(yōu)化后的效率是一樣的。但若是換成早期的編譯器,或是更加復雜的例子,效率和未開優(yōu)化的情況基本是一致的。

可以換一種編譯器,并降低其版本來觀察不同優(yōu)化級別下的表現(xiàn)。編譯器切換為 Clang 10.0。

O0 級別優(yōu)化,對比結(jié)果如下圖所示。

b28e86df41c23b1aef74b01731e43e32.png

O1 級別優(yōu)化,對比結(jié)果如下圖所示。

05c807e14c8eb36af308d7e5628d9359.png

O2 級別優(yōu)化效果,結(jié)果如下圖所示。

091bc55131c21a01681ca55cbd872666.png

到這個優(yōu)化級別,四種方式的性能已經(jīng)持平。

雖說不同編譯器的數(shù)值有所差異,但對比結(jié)果的整體趨勢基本一致。這個結(jié)果表明 std::bind 的確是性能殺手,應該優(yōu)先使用 Lambda 或 std::bind_front 代替。

Lambda vs.Functor

Lambda 就是一個可以攜帶狀態(tài)的函數(shù)。

其實現(xiàn)是一個含有 operator() 重載的匿名類,捕獲的參數(shù)作為匿名類的數(shù)據(jù)成員直接初始化。Lambda 使用時調(diào)用的便是這個重載的 operator(),返回的類型就是匿名類的類型,稱為 closure type。

Lambda 就是為簡化仿函數(shù)(即函數(shù)對象)而來,無需在其他地方創(chuàng)建一個仿函數(shù),直接原地構(gòu)造。因此,它們的性能基本是一致的。

加上以下測試代碼,和前面的 Lambda 代碼進行對比,驗證結(jié)果。

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

對比結(jié)果如下圖所示。

wKgZomcV6m6AKO9uAAA2xfGnYUw589.png

結(jié)果表明結(jié)論正確。

Lambda vs std::function

Lambda 和 std::function得分兩種情況進行對比,一種是無需存儲可調(diào)用體,一種是需要存儲可調(diào)用體。

先看第一種情況,測試代碼為:

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

測試環(huán)境依舊是 GCC 13.2,不開優(yōu)化。對比結(jié)果如下圖。

1b488fecca6f4cf8d1a9974d387e00e8.png

由此可知,Lambda 的開銷要比 std::function 小很多,應該優(yōu)先使用泛型 Lambda 傳遞可調(diào)用體。

再來看第二種情況,這種情況需要存儲可調(diào)用體,然而 Lambda 為 Closure type,只有使用 auto 或 decltype()才能推導出具體類型,它是無法存儲的。

此時,你只能使用std::function 或函數(shù)指針。具體使用哪種方式,便需要在性能、便捷性、靈活性等方面作出取舍。若是傾向于最大的便捷性和靈活性,前者是更好的選擇;若是追求最大化性能,函數(shù)指針則是更好的方式。但需注意,若是選擇函數(shù)指針,調(diào)用者將無法再使用 Lambda 和std::bind等常見方式傳遞參數(shù)。

Lambda vs.Function Pointer

對比內(nèi)容前文已涉,本節(jié)作為補充。

Lambda 是可以隱式轉(zhuǎn)換為函數(shù)指針的,只需將形式寫成 +[]{}(注意不能捕獲狀態(tài))。其性能與函數(shù)指針無異,這也是 Lambda 被廣泛使用的原因之一。Lambda 也可以攜帶狀態(tài),并和 std::invocable Concept 配合起來接受可調(diào)用對象,集靈活性和高性能于一身。

函數(shù)指針涉及間接調(diào)用,無法被編譯器優(yōu)化,是以既無法內(nèi)聯(lián),也無法重新排序。它可能指向不同的函數(shù),編譯器無法優(yōu)化這些調(diào)用的具體細節(jié),必須按照特定的調(diào)用約定進行處理。而 Lambda 在編譯時就可知道具體實現(xiàn),編譯器可以直接生成高效的調(diào)用代碼,無需遵循通用的調(diào)用約定,這將帶來巨大的優(yōu)化空間。

此外,只要滿足 constexpr function 的條件,Lambda 會隱式 constexpr,因此可以在編譯期評估。

編譯時間、內(nèi)存占用、指令讀取:std::bind vs. std::bind_front vs. Lambda

前文只是對比了這些方式在運行時間方面的性能,本節(jié)再對比編譯時間和內(nèi)存占用。

對比示例,代碼如下:

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

intadd(intx,inty){
//std::cout<
#include

intadd(intx,inty){
//std::cout<
#include

intadd(intx,inty){
//std::cout<

首先,來看編譯時間和內(nèi)存占用情況。如下圖所示。

17f33838b2278ae952baaba414b82f21.png

可以看到,Lambda 消耗的時間最短,只有 1.27 秒,Bind 消耗的時間最多,1.34 秒;Lambda 的最大常駐內(nèi)存大小為 96640KB,Bind Front 為 98436KB,而 Bind 是 100100KB。

其次,再來對比一下它們的指令讀取情況。如下圖。

c96bfc5bc2e602e883be27ad2fa3f5fa.png

其中,Lambda 運行期間指令總共讀取了32,265,989 次,Bind 是 390,268,192 次,而 Bind Front 是 262,267,908 次。可見,Lambda 比其他兩種方式的指令讀取次數(shù)少了一個數(shù)量級,Bind Front 較 Bind 也減少了非常多次。

最后,不難得出,無論是在運行時間,還是編譯時間、內(nèi)存占用和指令讀取方面,Lambda 的性能都是最好的,其次是 Bind Front,最后是 Bind。

總結(jié)

本文全面對比了 Lambda、std::bind、std::bind_front、std::function 和函數(shù)指針的性能,針對不同場景分析不同方式的優(yōu)劣,以能夠根據(jù)場景靈活選擇適當?shù)膶崿F(xiàn)方式。

Lambda 的性能(運行時間、編譯時間、內(nèi)存占用、指令讀取總數(shù))最好,和函數(shù)指針基本持平,其次是 std::bind_front,最后是 std::bind。std::bind 是失敗的設計,任何時候,都要優(yōu)先使用 Lambda 或 std::bind_front。

當不需要具體的可調(diào)用對象類型時,使用模板和 Lambda 的方式要優(yōu)于 std::function,其保留了靈活性和高性能;當需要具體的類型時,std::function 能夠提供最大的靈活性和便捷性,此時若想追求最大化性能,可考慮函數(shù)指針(將失去所有靈活性)。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • C++
    C++
    +關(guān)注

    關(guān)注

    21

    文章

    2090

    瀏覽量

    73404
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4694

    瀏覽量

    68075
  • 函數(shù)指針
    +關(guān)注

    關(guān)注

    2

    文章

    56

    瀏覽量

    3766
  • Lambda
    +關(guān)注

    關(guān)注

    0

    文章

    27

    瀏覽量

    9847

原文標題:Lambda, bind(front), std::function, Function Pointer Benchmarks

文章出處:【微信號:CPP開發(fā)者,微信公眾號:CPP開發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    函數(shù)指針指針函數(shù)的概念

    不少朋友會混淆“函數(shù)指針”和“指針函數(shù)”這兩個概念,本文詳細介紹一下。
    發(fā)表于 03-09 10:49 ?1111次閱讀

    Nanopi系列板子資源性能對比

    Nanopi系列板子資源性能對比對比性能 選擇適合你的板子
    發(fā)表于 08-05 14:21

    SparkRDMA基于BigDataBench的性能對比測試

    SparkRDMA基于BigDataBench 性能對比測試
    發(fā)表于 05-04 13:16

    Linux下AWTK與Qt的性能對比

    為了比較直觀的看到AWTK的基本性能,我們對產(chǎn)品開發(fā)者比較關(guān)心GUI的一些參數(shù)做了測試,如界面刷新幀數(shù)、啟動時間等。讓我們從參數(shù)上直觀了解Linux下AWTK與Qt的性能對比。
    發(fā)表于 10-29 08:26

    lambda函數(shù)基礎(chǔ)

    lambda函數(shù)基礎(chǔ)lambda與def
    發(fā)表于 12-29 06:22

    Arm Cortex-A35性能對比分析

    Arm Cortex-A35性能對比
    發(fā)表于 01-19 07:44

    arduino和stm32性能對比究竟誰更厲害?

    一些DIY和各種小項目?arduino和stm32性能對比究竟誰更厲害呢?我們一起來討論一下。比較兩者之前首先我們來了解下arduino和stm32的特點:Arduino:Arduino UNO-DFRobot商城1. Arduino更傾向于創(chuàng)意,它弱化了具體的硬件的操作,它的
    發(fā)表于 01-24 07:14

    函數(shù)指針指針函數(shù)定義

    函數(shù)指針指針函數(shù),C語言學習中最容易混淆的一些概念,好好學習吧
    發(fā)表于 01-11 16:44 ?0次下載

    C語言指針函數(shù)函數(shù)指針詳細介紹

    C語言指針函數(shù)函數(shù)指針詳細介紹。。。。。。。
    發(fā)表于 03-04 15:27 ?5次下載

    c語言函數(shù)指針定義,指針函數(shù)函數(shù)指針的區(qū)別

     往往,我們一提到指針函數(shù)函數(shù)指針的時候,就有很多人弄不懂。下面就由小編詳細為大家介紹C語言中函數(shù)指針
    發(fā)表于 11-16 15:18 ?3594次閱讀

    理解函數(shù)指針函數(shù)指針數(shù)組、函數(shù)指針數(shù)組的指針

    理解函數(shù)指針、函數(shù)指針數(shù)組、函數(shù)指針數(shù)組的指針
    的頭像 發(fā)表于 06-29 15:38 ?1.5w次閱讀
    理解<b class='flag-5'>函數(shù)</b><b class='flag-5'>指針</b>、<b class='flag-5'>函數(shù)</b><b class='flag-5'>指針</b>數(shù)組、<b class='flag-5'>函數(shù)</b><b class='flag-5'>指針</b>數(shù)組的<b class='flag-5'>指針</b>

    指針進階第五站:函數(shù)指針

    定義一個函數(shù)指針,指向的函數(shù)有兩個int形參并且返回一個函數(shù)指針,返回的指針指向一個有一個int
    發(fā)表于 08-17 15:58 ?501次閱讀

    什么是Lambda函數(shù)

    今天來給大家推薦一個 Python 當中超級好用的內(nèi)置函數(shù),那便是 lambda 方法,本篇教程大致和大家分享 什么是 lambda 函數(shù) lamb
    的頭像 發(fā)表于 10-17 11:27 ?1103次閱讀

    函數(shù)指針指針函數(shù)是不是一個東西?

    函數(shù)指針的本質(zhì)是指針,就跟整型指針、字符指針一樣,函數(shù)指針
    的頭像 發(fā)表于 01-03 16:35 ?468次閱讀
    <b class='flag-5'>函數(shù)</b><b class='flag-5'>指針</b>和<b class='flag-5'>指針</b><b class='flag-5'>函數(shù)</b>是不是一個東西?

    面試常考+1:函數(shù)指針指針函數(shù)、數(shù)組指針指針數(shù)組

    在嵌入式開發(fā)領(lǐng)域,函數(shù)指針、指針函數(shù)、數(shù)組指針指針數(shù)組是一些非常重要但又容易混淆的概念。理解它
    的頭像 發(fā)表于 08-10 08:11 ?505次閱讀
    面試???1:<b class='flag-5'>函數(shù)</b><b class='flag-5'>指針</b>與<b class='flag-5'>指針</b><b class='flag-5'>函數(shù)</b>、數(shù)組<b class='flag-5'>指針</b>與<b class='flag-5'>指針</b>數(shù)組