引言
很多时候,选择单一,事情做来不会有多少阻力,选择太多 ,倒是举棋难定了。
C++ 复杂性的一方面就体现在选择太多,对于同一种需求,可能存在数十种不同的方式都能够解决,此时每种方式的优劣便是学习的难点。
std::function
, 函数指针,
std::bind
, Lambda 就是这样的一些组件,使用频率不低,差异细微,许多人不清楚何时使用何种方式,常常误用,致使程序性能出现瓶颈。
本文全面地对比了这些组件间的细微差异,并评估不同方式的性能优劣,提出使用建议及一些实践经验。
首先要明确谁与谁对比,理清可替代对象,这样对比起来才有意义。
std::function
的对比对象是函数指针,它们主要是为了支持函数的延迟调用;
std::bind
的对比
对象是
Lambda 和
std::bind_front
,主要是为了支持参数化绑定。
本文会全面对比这些方式的运行时间、编译时间、内存占用和指令读取总数。
旧事
函数若是不想被立即执行,在 C 及 C++11 以前存在许多方式,函数指针是最普遍的一种方式。看个例子:
void foo (int x) { std ::cout << "Function called with " << x << '\n' ; }void bar (void (*pf) (int ) , int value) { pf(value); // delayed invocation }int main () { bar(foo, 10 ); }
通过函数指针实现了函数的延迟调用,这在回调函数、事件处理、惰性计算等场景下被广泛使用。C++11 之前,提供了仿函数来代替函数指针,于是上述示例可以等价写成:
struct functor { void operator () (int x) const { std ::cout << "Function called with " << x << '\n' ; } };void bar (const functor& func, int value) { func(value); // delayed invocation }int main () { bar(functor(), 10 ); }
相比函数指针,仿函数具有更好的灵活性和安全性,它可以持有状态,可以有成员函数和成员变量,并且更加容易被编译器优化。而函数指针涉及间接调用,编译器不会对其进行内联优化,还有可能出现类型转换错误。
由于函数指针无法持有状态,C 里面一般会增加一个状态参数来捕获状态,例如:
typedef int (*add_pf) (void *, int ) ;int add_with_state (void * state, int x) { int increment = *(int *)state; return x + increment; }int bar (add_pf func, void *state, int value) { return func(state, value); // delayed invocation }int main () { int increment = 5
; add_pf add = add_with_state; return bar(add, &increment, 10 ); // return 15 }
仿函数则稍微简单一点,等价写法为:
class add_functor { int increment;public : add_functor(int inc) : increment(inc) {} int operator () (int x) const { return x + increment; } };int bar (const add_functor& func, int value) { return func(value); // delayed invocation }int main () { add_functor add (5 ) ; return bar(add, 10 ); // return 15 }
相较之下,仿函数捕获状态方便很多,语法也更加清晰简洁。
早期 C++ 还提供
std::bind1st
和
std::bind2nd
来绑定函数,以下是一个例子:
int add (int x, int y) { return x + y; }int main () { auto bound_func = std ::bind1st(std ::ptr_fun(add), 5 ); return bound_func(10 ); // return 15 }
不过如今都已废弃,
std::bind1st
被
std::bind
代码,
std::ptr_fun
被
std::function
代替。
旧事且过,来看新的方法。
std::function vs. Function pointer
std::function
是 C++11 对于可调用体的高度抽象组件,不仅能够持有普通函数和成员函数,还能够持有仿函数、Lambda 和其他类型的可调用体。
一个组件的抽象层次越高,考虑的越周全,额外的工作也就越多,开销也会更大。
下面通过一个简单的例子,对比一下
std::function
和函数指针的生成代码。
//////////////////////////////// // function pointer int add (int x, int y) { return x + y; }int bar (int (*func) (int , int ) , int x, int y) { return func(x, y); }int main () { return bar(add, 5 , 10 ); // return 15 }////////////////////////////////
// std::function int add (int x, int y) { return x + y; }int bar (std ::function<int (int , int ) > func, int x, int y) { return func(x, y); }int main () { return bar(add, 5 , 10 ); // return 15 }
在 GCC 13.2 最高级别的优化下,函数指针( https://godbolt.org/z/vno8WaYTK )生成的汇编代码只有 11 行,而
std::function
( https://godbolt.org/z/W71bWo3qj )生成的却有 60 行,差异巨大。
实际 Benchmarks 一下,测试代码为:
int add (int x, int y) { return x + y; }int bar_function_ptr (int (*func) (int , int ) , int x, int y) { return func(x, y); }int bar_function (std ::function<int (int , int ) > func, int x, int y) { return func(x, y); }static void function_ptr_bench (benchmark::State& state) { for (auto _ : state) { int result = bar_function_ptr(add, 5 , 10 ); benchmark::DoNotOptimize(result); } } BENCHMARK(function_ptr_bench);static void function_bench (benchmark::State& state) { for (auto _ : state) { int result = bar_function(add, 5 , 10 ); benchmark::DoNotOptimize(result); } } BENCHMARK(function_bench);
结果不出所料,
std::function
的运行开销要远远大于函数指针。
func-ptr-vs-function-benchmarks
既然函数指针效率这么高,那还要
std::function
干嘛?
除了旧事一节提到的关于函数指针的缺点,还有一个很大的不同在于一致性,
std::function
能持有普通函数、成员函数、仿函数、Lambda 等等可调用体,灵活性突出,函数指针可没有这个能力,是以适用性更低。
请注意,尽管本节的对比结果表明函数指针效率更高,但却并非是说推荐使用函数指针。
std::bind vs. 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 int add (int x, int y) { return x + y; }typedef int (*pf) (int , int ) ;static void func_ptr (benchmark::State& state) { int val = 42 ; pf add_func = add; for (auto _ : state) { int result = add_func(val, 10 ); benchmark::DoNotOptimize(result); } } BENCHMARK(func_ptr);static void lambda (benchmark::State& state) { int val = 42 ; const auto lam = [val](int y) { return val + y; }; for (auto _ : state) { int result = lam(10 ); benchmark::DoNotOptimize(result); } } BENCHMARK(lambda);static void bind (benchmark::State& state) { int val = 42 ; const auto bind = std ::bind(add, val, std ::placeholders::_1); for (auto _ : state) { int result = bind(10 ); benchmark::DoNotOptimize(result); } } BENCHMARK(bind);static void bind_front (benchmark::State& state) { int val = 42 ; const auto bind = std ::bind_front(add, val); for (auto _ : state) { int result = bind(10 ); benchmark::DoNotOptimize(result); } } BENCHMARK(bind_front);
编译器 GCC 13.2,不开优化,对比结果如下图所示。
可见,
在设计上,
Lambda 并不会比函数指针更慢,
而
std::bind
却将近慢了二十倍,
std::bind_front
则比
std::bind
效率高许多,只慢了近十倍。
注意这是在未开优化的情况下,事实上,如今的编译器优化能力很强,示例相对过于简单,优化后的效率是一样的。但若是换成早期的编译器,或是更加复杂的例子,效率和未开优化的情况基本是一致的。
可以换一种编译器,并降低其版本来观察不同优化级别下的表现。编译器切换为 Clang 10.0。
O0 级别优化,对比结果如下图所示。
O1 级别优化,对比结果如下图所示。
O2 级别优化效果,结果如下图所示。
到这个优化级别,四种方式的性能已经持平。
虽说不同编译器的数值有所差异,但对比结果的整体趋势基本一致。
这个结果表明
std::bind
的确是性能杀手,应该优先使用 Lambda 或
std::bind_front
代替。
Lambda vs. Functor
Lambda 就是一个可以携带状态的函数。
其实现是一个含有
operator()
重载的匿名类,捕获的参数作为匿名类的数据成员直接初始化。Lambda 使用时调用的便是这个重载的
operator()
,返回的类型就是匿名类的类型,称为 closure type。
Lambda 就是为简化仿函数(即函数对象)而来,无需在其他地方创建一个仿函数,直接原地构造
。因此,它们的性能基本是一致的。
加上以下测试代码,和前面的 Lambda 代码进行对比,验证结果。
struct Functor { int x; auto operator () (int y) const { return x + y; } };static void functor (benchmark::State& state) { int val = 42 ; Functor functor (val) ; for (auto _ : state) { int result = functor(10 ); benchmark::DoNotOptimize(result); } } BENCHMARK(functor);
对比结果如下图所示。
结果表明结论正确。
Lambda vs std::function
Lambda 和
std::function
得分两种情况进行对比,一种是无需存储可调用体,一种是需要存储可调用体。
先看第一种情况,测试代码为:
int callable_with_lambda (auto func) { return func(1 , 2 ); }int callable_with_funtional (std ::function<int (int , int ) > func) { return func(1 , 2 ); }static void pass_callable_with_lambda (benchmark::State& state) { for (auto _ : state) { int result = callable_with_lambda([](int a, int b) { return a + b; }); benchmark::DoNotOptimize(result); } } BENCHMARK(pass_callable_with_lambda);static void pass_callable_with_funtional (benchmark::State& state) { for (auto _ : state) { int result = callable_with_funtional([](int a, int b) { return a + b; }); benchmark::DoNotOptimize(result); } } BENCHMARK(pass_callable_with_funtional);
测试环境依旧是 GCC 13.2,不开优化。对比结果如下图。