Effective STL第46条:考虑使用函数对象而不是函数作为STL算法的参数
书里说到:
你或许会对本条款将要讨论的话题感到惊讶:将函数对象(即可以被伪装成函数的对象)传递给STL算法往往比传递实际的函数更高效。
书中提到了性能比较的数值:
我在4个不同的STL平台上,针对一个包含100w个double数据的vector对象测试了两种调用,并都设置了速度优化,
……最差的情况下,使用函数对象的版本快了50%,最好的情况下快了160%
书中进行了解释:
1.函数对象时,编译器将该函数内联进去了
2.使用函数时,编译器会隐式的将它转化成一个指向该函数的指针,并将该指针传递过去,而大多数编译器不会对函数指针执行的函数进行内联优化
看书100本不如写代码1行。这里记录下整个理解过程。
在实际操作中,经常是用函数直接传给STL的,像下面这样:
#define NUM 10000000
typedef std::vector<double> VecData;
typedef VecData::iterator VecDataIT;
//排序函数
inline bool doubleGreater(double d1, double d2)
{
return d1 > d2;
}
VecData data(NUM),dataplus(NUM);
std::generate(data.begin(), data.end(), rand);
//用函数做STL的参数
std::sort(data.begin(), data.end(), doubleGreater);
//引用库
#include <functional>
调用的时候
std::sort(data.begin(), data.end(), std::greater<double>());
采用高精度计时器类写的1ms精度定时器,
判断transform前后所需时间。
测试平台如下
Win10 64bit
VS2015 默认优化
基于对话框 Release模式
耗时取10次平均值
方法 | 耗时(ms) |
---|---|
函数对象 | 900 |
函数 | 1600 |
结论
和书中预言的一样,使用函数对象,也就是特殊的类,反而更快。(在csdn上有人说传递函数实际上在汇编里只有2个命令,而函数对象构造一个类至少要50个命令,
但结果就是传递类进去更快)
这里函数对象时调用的库函数,库函数一般都是大神优化很多的结果,如果是自己写的函数对象呢,还能一样对函数碾压吗?
下面继续试验,试验自定义函数和函数对象,同样做1000w浮点排序
//自定义类
typedef struct _tData
{
double data1;
double data2;
_tData()
{
data1 = rand();
data2 = rand();
}
inline _tData& operator= (_tData& d)
{
data1 = d.data1;
data2 = d.data2;
return(*this);
}
inline bool operator<(_tData& d)
{
return(data1 < d.data1);
}
}Data;
typedef std::vector<Data> VecData;
//比较函数
inline bool doubleGreater(Data& d1, Data& d2)
{
return d1.data1 > d2.data1;
}
排序时
std::sort(data.begin(), data.end());//doubleGreater
测试结果
方法 | 耗时(ms) |
---|---|
函数对象 | 930 |
函数 | 1670 |
结论
还是一样,使用函数对象,反而更快。
将一个double向量里的数据经过一个复杂的运算赋值到另外一个double向量里去,这里对比3种方法效率
VecDouble data(NUM),datacalc(NUM);
srand((unsigned)time(NULL));
std::generate(data.begin(), data.end(), rand);
for (UINT i = 0; i < data.size(); i++)
{ datacalc.push_back(sqrt(data[i] * 3.552 / 2.1 + 66.3)); }
inline double calc(double data)
{
return(sqrt(data * 3.552 / 2.1 + 66.3));
}
调用:
std::transform(data.begin(), data.end(), datacalc.begin(), Calc());
typedef struct _Calc
{
double operator()(const double data)
{
return(sqrt(data * 3.552 / 2.1 + 66.3));
}
}Calc;
调用:
std::transform(data.begin(), data.end(), datacalc.begin(), Calc());
测试结果
方法 | 耗时(ms) |
---|---|
for循环 | 270 |
函数对象 | 58 |
内联函数 | 59 |
结论
STL太牛逼了,甩一直用的for循环几条街都不止,以后能用STL的都要用STL。
Effective STL第43条
算法调用优先于手写的循环
作者给出了3个理由
1.效率
2.正确性
3.可维护性
上面提到的都是简单的不带参数函数对象,在STL的算法里很多是可以带参数的函数对象。
Effective STL第40条
若一个类是函数子,则应使它可配接
书中写到:
在STL中,4个标准的函数配接器(not1,not2,bind1st,bind2nd)都要求特殊的类型定义,而非标的,boost提供的模板很多也包含了这样的组件。提供这些类型定义最简便的方法便是让函数子从特定的基类继承。
如果函数子类的operator()
只有一个实参,那么它应该从std::unary_function
继承
如果函数子类的operator()
有两个实参,那么它应该从std::binary_function
继承
具体为:
对于
std::unary_function
,必须指定函数子类operator()
所带的参数的类型,以及返回类型。
对于std::binary_function
,必须指定三个类型:operator()
的第一个和第二个参数的类型函数子类所带的参数的类型,以及返回类型。
将上面的一个结构改为下面
typedef struct _Calc : public std::unary_function<double,double>
{
double operator()(const double data)
{
return(sqrt(data * 3.552 / 2.1 + 66.3));
}
}Calc;
注意: std::unary_function<double,double>
里第一个double
是operator()
的输入参数类型,第二个double
是operator()
的返回类型。
书中原话:
一般情况下,传递给
std::unary_function
和std::binary_function
的非指针类型要去掉const
和引用&
;
而如果operator()
带有指针,则传递给std::unary_function
和std::binary_function
的类型也要为指针。