专栏名称: CPP开发者
伯乐在线旗下账号,「CPP开发者」专注分享 C/C++ 开发相关的技术文章和工具资源。
目录
相关文章推荐
湖北经视  ·  “破门亮灯”,官方致歉 ·  昨天  
山西省人民政府  ·  哈尔滨亚冬会今晚开幕!山西选手姜鑫杰出战单板滑雪 ·  2 天前  
兵团零距离  ·  哈尔滨亚冬会 | 宁忠岩、刘梦婷将担任旗手! ·  3 天前  
湖北经视  ·  突发讣告!感动众人…… ·  3 天前  
51好读  ›  专栏  ›  CPP开发者

Lambda, bind(front), std::function, Function Pointer Benchmarks

CPP开发者  · 公众号  ·  · 2024-06-24 11:50

正文

引言

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

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)(intint)int x, int y) {
    return func(x, y);
}

int main() {
    return bar(add, 510); // return 15
}

////////////////////////////////
// std::function
int add(int x, int y) {
    return x + y;
}

int bar(std::function<int(intint)> func, int x, int y) {
    return func(x, y);
}

int main() {
    return bar(add, 510); // 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)(intint)int x, int y) {
    return func(x, y);
}

int bar_function(std::function<int(intint)> 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, 510);
        benchmark::DoNotOptimize(result);
    }
}
BENCHMARK(function_ptr_bench);

static void function_bench(benchmark::State& state) {
    for (auto _ : state) {
        int result = bar_function(add, 510);
        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)(intint);

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(12);
}

int callable_with_funtional(std::function<int(intint)> func) {
    return func(12);
}

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,不开优化。对比结果如下图。







请到「今天看啥」查看全文