技术: benchmark(lambda 和 bind)

比较一下 Bind 和 Lambda 的性能.

yasnippet 进行代码补全(实现上是采用的模板的形式), 但是每次补全的算法模板, 都是采用的lambda形式代替函数对象(functor), 于是引起了我极大的兴趣.

lambda表达式, 比起 bind 和 function 运行效率如何? 如果仅仅是简单分析, lambda省去了运行时上下文切换的时间, 应该是效率会更高(实际上匿名代码块, 写起来也简单啊).

究竟效率如何? 仔细研究一下.

结论: 多用lambda表达式

post-cover

引子

前面已经专门说过 functionbind 了, 现在要看看lambda表达式作为调用对象, 以及bind后的functor, std::function生成的调用对象, 哪个效率最高.

正文

代码

先把代码贴出来, 后面再分析.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
  
// Turn building and testing boost::bind on or off with this macro
#define USE_BOOST 1

// workaround for varieties of g++-4.6 with --std=gnu++0x
#ifndef _GLIBCXX_USE_NANOSLEEP
# define _GLIBCXX_USE_NANOSLEEP
#endif

#include <cstdint>
#include <chrono>
#include <iostream>
#include <string>
#include <thread>

#if USE_BOOST
#include <boost/function.hpp>
#include <boost/bind.hpp>
#endif

class timer
{
public:
typedef std::chrono::high_resolution_clock clock;
typedef clock::time_point time_point;
typedef clock::duration duration;

public:
timer()
{
reset();
}

void reset()
{
_starttime = clock::now();
}

duration elapsed() const
{
return clock::now() - _starttime;
}
protected:
time_point _starttime;
};

bool test_timer()
{
using std::chrono::milliseconds;
typedef timer::duration duration;

const milliseconds sleep_time(500);

timer t;
std::this_thread::sleep_for(sleep_time);
duration recorded = t.elapsed();

// make sure the clock and this_thread::sleep_for is precise within one millisecond (or at least in agreement as to
// how inaccurate they are)
return (recorded - milliseconds(1) < sleep_time)
&& (recorded + milliseconds(1) > sleep_time);
}

template <typename T>
void volatile_write(const T& x)
{
volatile T* p = new T;
*p = x;
delete p;
}

template <typename Function>
void run_test(const std::string& name, Function func)
{
std::cout << name;
timer t;
volatile_write(func());
timer::duration duration = t.elapsed();
std::cout << '\t' << duration.count() << std::endl;
}

template <typename Function>
void do_test_loop(Function func, const uint64_t upper_limit = 1000000000ULL)
{
for (uint64_t i = 0; i < upper_limit; ++i)
func(i);
}

uint64_t test_accumulate_lambda()
{
uint64_t x = 0;
auto accumulator = [&x] (uint64_t i) { x += i; };
do_test_loop(accumulator);
return x;
}

void test_accumulate_bind_function(uint64_t& x, uint64_t i)
{
x += i;
}

uint64_t test_accumulate_bind()
{
namespace arg = std::placeholders;

uint64_t x = 0;
std::function<void (uint64_t)> accumulator = std::bind(&test_accumulate_bind_function, std::ref(x), arg::_1);
do_test_loop(accumulator);
return x;
}

uint64_t test_accumulate_bound_lambda()
{
uint64_t x = 0;
std::function<void (uint64_t)> accumulator = [&x] (uint64_t i) { x += i; };
do_test_loop(accumulator);
return x;
}

#if USE_BOOST
uint64_t test_accumulate_boost_bind()
{
uint64_t x = 0;

boost::function<void (uint64_t)> accumulator = boost::bind(&test_accumulate_bind_function, boost::ref(x), _1);
do_test_loop(accumulator);
return x;
}

uint64_t test_accumulate_boost_bound_lambda()
{
uint64_t x = 0;
boost::function<void (uint64_t)> accumulator = [&x] (uint64_t i) { x += i; };
do_test_loop(accumulator);
return x;
}
#endif

int main()
{
if (!test_timer())
{
std::cout << "Failed timer test." << std::endl;
return -1;
}

run_test("Accumulate (lambda) ", &test_accumulate_lambda);
run_test("Accumulate (bind) ", &test_accumulate_bind);
run_test("Accumulate (bound lambda) ", &test_accumulate_bound_lambda);
#if USE_BOOST
run_test("Accumulate (boost bind) ", &test_accumulate_boost_bind);
run_test("Accumulate (boost bound lambda)", &test_accumulate_bound_lambda);
#endif
}

分析

其实测试就是简单的把一个函数跑 10次, 记录一下时间.

循环调用functor的函数如下:

1
2
3
4
5
6
template <typename Function>  
void do_test_loop(Function func, const uint64_t upper_limit = 1000000000ULL)
{
for (uint64_t i = 0; i < upper_limit; ++i)
func(i);
}

但是注意, 这里的func是一元函数对象, 所以当你给出一个二元函数的时候, 需要用到 std::bind 进行参数绑定.

对于bind的情况是这样的:(把二元函数对象, 先转换为一元函数对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//二元函数对象, 其中x属于外部捕获, 保存最终计算结果
void test_accumulate_bind_function(uint64_t& x, uint64_t i)
{
x += i;
}

//主测函数
uint64_t test_accumulate_bind()
{
namespace arg = std::placeholders;

uint64_t x = 0;
std::function<void (uint64_t)> accumulator
= std::bind(&test_accumulate_bind_function,
std::ref(x), arg::_1);
do_test_loop(accumulator); //跑10亿次
return x;
}

但是对于lambda表达式, 可以直接传入代码块, 那么 主测函数 就会简单的多:

1
2
3
4
5
6
7
uint64_t test_accumulate_lambda()  
{
uint64_t x = 0;
auto accumulator = [&x] (uint64_t i) { x += i; }; //注意auto
do_test_loop(accumulator);
return x;
}

注意上面是用 auto 接收的, 也就是说, 很可能在编译的时候就会被优化掉(省去运行时的上下文切换).

如果用一定要让 lambda 在运行时调用, 那么可以用 std::function 指定调用入口, 之后运行时再进行判断, 代码如下:

1
2
3
4
5
6
7
8
uint64_t test_accumulate_bound_lambda()  
{
uint64_t x = 0;
std::function<void (uint64_t)> accumulator
= [&x] (uint64_t i) { x += i; }; //std::function
do_test_loop(accumulator);
return x;
}

然后依次把它们传递给 run_test 测试引擎:

  • test_accumulate_bind
  • test_accumulate_lambda
  • test_accumulate_bound_lambda (这里用std::function 给定运行时调用入口)

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename Function>  
void run_test(const std::string& name, Function func)
{
std::cout << name;
timer t;
volatile_write(func());
timer::duration duration = t.elapsed();
std::cout << '\t' << duration.count() << std::endl;
}

int main()
{
run_test("Accumulate (lambda) ", &test_accumulate_lambda);
run_test("Accumulate (bind) ", &test_accumulate_bind);
run_test("Accumulate (bound lambda)", &test_accumulate_bound_lambda);
}

测试结果是:

1
2
3
Accumulate (lambda)               7
Accumulate (bind) 4401849
Accumulate (bound lambda) 4379315

其实还是差的蛮远的, 并且一旦用到了std::function, 即把可调用对象用一个统一的接口封装起来, 可能还是需要进行栈的上下文切换.

gdb disassemble一下可以看看:

1
2
3
4
5
(gdb) disassemble test_accumulate_lambda
Dump of assembler code for function _Z22test_accumulate_lambdav:
0x0000000000400e70 <+0>: movabs $0x6f05b59b5e49b00,%rax
0x0000000000400e75 <+5>: retq
End of assembler dump.

明显的看出来, 是被编译器优化了(如果有栈切换, 关键代码不能只有这么少): 整个函数仅仅是将0x6f05b59b5e49b00(十进制值为:499999999500000000)移动到了rax寄存器中就返回了. 编译器非常智能的知道了我们仅仅是对0到1000000000之间的数字求和并直接帮我们进行了代码替换的优化, 相当于:

1
2
3
4
5
6
7
8
uint64_t test_accumulate_lambda()  
{
uint64_t x = 0;
// do_test_loop:
for (uint64_t i = 0; i < 1000000000; ++i)
x += i;
return x;
}

编译器知道lambda函数是具有静态性的,因此你可以放心的使用lambda函数而不必担心它性能。那么我们调用的std::function又是怎样的一个过程呢?在这里它的多态性让我们很难去剖析,当函数do_test_loop被函数std::function实例化时,编译器并不知道func的行为,因此它能做任何事情(它只是std::function的入口点)。std::bind和lambda表达式之间的不同之处是极其细微的。(std::function影响性能)

结论已经出来了, 不管bind和function如何, 直接使用lambda性能会非常高.

补充

一般编译器会把一些没有做任何事情, 或者可以优化的内容优化掉, 比如loop的步长啊, 累加啊等(具体的可以参考gcc手册), 但是原文作者用了这样的一个函数防止优化:

1
2
3
4
5
6
7
template <typename T>  
void volatile_write(const T& x)
{
volatile T* p = new T;
*p = x;
delete p;
}

把测试函数的执行结果写入, 强制写入, 从而避免编译器认为这货什么都没有做.

尾巴

本文相当于在前几篇, lambda, bind, function的基础上, 做了一个选择, 以后自己的代码补全模板中, 尽可能的使用 lambda.

参考资料

  1. http://www.gockelhut.com/c++/articles/lambda_vs_bind
文章目录
  1. 1. 引子
  2. 2. 正文
    1. 2.1. 代码
    2. 2.2. 分析
    3. 2.3. 补充
  3. 3. 尾巴
  4. 4. 参考资料
|