C++11:改善程序性能

1 右值引用

1.1 右值引用的特性

C++11中所有的值必属于左值、将亡值、纯右值三者之一。将亡值和纯右值都属于右值。区分表达式的左右值属性有一个简便方法:若可对表达式用&符取址,则为左值,否则为右值。
比如,简单的赋值语句:

int i = 0;

在这条语句中,i是左值,0是字面量,就是右值。在上面的代码中,i可以被引用,0就不可以了。字面量都是右值。
右值引用就是对一个右值进行引用的类型。因为右值不具名,所以我们只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。

void Printvalue (int& i)
{
	std::cout<<"lvalue : "<<i<<std::endl;
}

void Printvalue (int& & i)
{
	std::cout<<"rvalue : "<<i<<std::endl;
}

void Forward (int& & i)
{
	Printvalue (i);
}

int main ()
{
	int i = 0;
	Printvalue(i);
	Printvalue(1);
	Forward (2);
}

将输出如下结果:
lvalue : 0
rvalue : 1
lvaue : 2
Forward函数接收的是一个右值,但在转发给PrintValue时又变成了左值,因为在Forward中调用PrintValue时,右值i变成了一个命名的对象,编译器会将其当作左值处理。

1.2 右值引用避免深拷贝

class A
{
public:
	A() :m_ptr (new int (0))
	{
		cout <<"construct"<< endl ;
	}
	
	A (const A& a) :m_ptr (new int(*a.m_ptr))	//深拷贝
	{
		cout << "copy construct" <<endl;
	}
	
	~A()
	{
		cout << "destruct" <<endl;
		delete m_ptr;
	}
	
private:
	int* m_ptr;
};

/为了避免返回值优化,此函数故意这样写
A Get (bool flag)
{
	A a;
	A b;
	if (flag)
		return a;
	else
		return b;
}

int main()
{
	A a = Get(false);	//运行正确
}

上面的代码将输出:
construct
construct
copy construct
destruct
destruct
destruct
这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的。上面代码中的Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?答案是肯定的。看下面的代码:

class A
{
public:
	A() :m_ptr (new int (0))
	{
		cout <<"construct"<< endl ;
	}
	
	A (const A& a) :m_ptr (new int(*a.m_ptr))	//深拷贝
	{
		cout << "copy construct" <<endl;
	}
	
	A(A&& a) :m_ptr (a.m_ptr)
	{
		a.m ptr = nullptr;
		cout << "move construct: "<<endl;
	}

	~A()
	{
		cout << "destruct" <<endl;
		delete m_ptr;
	}
	
private:
	int* m_ptr;
};

int main()
{
	A a = Get(false);	//运行正确
}

上面的代码将输出:
construct
construct
move construct
destruct
destruct
destruct
上面的代码中没有了拷贝构造,取而代之的是移动构造(Move Construct)。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的A&&用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义(move语义),右值引用的一个重要目的是用来支持移动语义的。
移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高C++应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。

2 move语义

move实际上并不能移动任何东西,它唯一的功能是将一个左值强制转换为一个右值引用°,使我们可以通过右值引用使用该值,以用于移动语义。强制转换为右值的目的是为了方便实现移动构造。
这种move语义是很有用的,比如一个对象中有一些指
针资源或者动态数组,在对象的赋值或者拷贝时就不需要拷贝这些资源了。在C++11之前拷贝构造函数和赋值函数可能要像下面这样定义。假设一个A对象内部有一个资源m _ptr:

A& A::operator=(const A& rhs)
{
	//销毁m_ptr指向的资源
	//复制rhs.m_ptr所指的资源,并使m_ptr指向它
}

同样A的拷贝构造函数也是这样。假设这样来使用A:

A foo(); 	//foo是一个返回值为×的函数
A a;
a = foo() ;

最后一行将会发生如下操作:销毁a所持有的资源;复制foo返回的临时对象所拥有的资源;销毁临时对象,释放其资源。
上面的过程是可行的,但是更有效率的办法是直接交换a和临时对象中的资源指针,然后让临时对象的析构函数去销毁a原来拥有的资源。换句话说,当赋值操作符的右边是右值的时候,我们希望赋值操作符被定义成下面这样:

A& A::operator=(const A&& rhs)
{
	//转移资源的控制权,无须复制
}

仅仅转移资源的所有者,将资源的拥有者改为被赋值者,这就是所谓的move语义。再看一个例子,假设一个临时容器很大,赋值给另一个容器。

{
	std::list<std::string> tokens;	//省略初始化……
	std::list<std::string> t = tokens;
}
std::list<std::string> tokens;
std::list<std::string> t = std::move(tokens);

如果不用std:move,拷贝的代价很大,性能较低。使用move几乎没有任何代价,只是转换了资源的所有权。实际上是将左值变成右值引用,然后应用move语义调用构造函数,就避免了拷贝,提高了程序性能。当一个对象内部有较大的堆内存或者动态数组时,很有必要写move语义的拷贝构造函数和赋值函数,避免无谓的深拷贝,以提高性能。事实上,C++中所有的容器都实现了move语义,方便我们实现性能优化。
这里也要注意对move语义的误解,move只是转移了资源的控制权,本质上是将左值强制转换为右值引用,以用于move语义,避免含有资源的对象发生无谓的拷贝。move对于拥有形如对内存、文件句柄等资源的成员的对象有效。如果是一些基本类型,比如 int 和char[10]数组等,如果使用move,仍然会发生拷贝(因为没有对应的移动构造函数),所以说move对于含资源的对象来说更有意义。

3 forward和完美转发

上节中介绍的右值引用类型是独立于值的,一个右值引用参数作为函数的形参,在函数内部再转发该参数的时候它已经变成一个左值了,并不是它原来的类型了。比如:

template <typename T>
void forwardvalue(T& val)
{
	processvalue(val);	//右值参数会变成左值
}

template <typename T>
void forwardvalue(const T& val)
{
	processvalue(val);	//参数都变成常量左值引用了
}

都不能按照参数的本来的类型进行转发。
因此,我们需要一种方法能按照参数原来的类型转发到另一个函数,这种转发被称为完美转发。所谓完美转发( Perfect Forwarding),是指在函数模板中,完全依照模板的参数的类型(即保持参数的左值、右值特征),将参数传递给函数模板中调用的另外一个函数。C++11中提供了这样的一个函数std::forward,它是为转发而生的,不管参数是T&&这种未定的引用还是明确的左值引用或者右值引用,它会按照参数本来的类型转发。看这个例子:

template <typename T>
void PrintT(int& t)
{
	std::cout << "lvalue" << std::endl;
}

template <typename T>
void PrintT(T&& t)
{
	std::cout << "rvalue" << std::endl;
}

template <typename T>
void TestForward(T&& v)
{
	PrintT(v);
	PrintT(std::forward<T>(v));
	PrintT(std::move(v));
}

void Test()
{
	TestForward(1);
	int x = 1;
	TestForward(x);
	TestForward(std::forward<int>(x));
}

输出结果:
lvalue
rvalue
rvalue
lvalue
lvalue
rvalue
lvalue
rvalue
rvalue
分析:
TestForward(1):由于1是右值,所以未定的引用类型T&& v被一个右值初始化后变成了一个右值引用,但是在TestForward函数体内部,调用PrintT(v)时,v又变成了一个左值(因为在这里它已经变成了一个具名的变量, 所以它是一个左值),因此第一个PrintT被调用,打印出“lvaue”。调用PrintT(std::forward(v))时,由于std::forward会按参数原来的类型转发,因此,它还是一个右值(这里已经发生了类型推导,所以这里的T&&不是一个未定的引用类型(关于这点可以参考2.1节),会调用void PrintT(T&& t)函数。调用PrintT(std::move(v))是将v变成一个右值(v本身也是右值),因此,它将输出rvalue。
TestForward(x):未定的引用类型T&& v被一个左值初始化后变成了一个左值引用,因此,在调用PrintT(std::forward(v))时它会被转发到 void PrintT(T& t)。

4 emplace_back 减少内存拷贝和移动

emplace_back能就地通过参数构造对象,不需要拷贝或者移动内存,相比 push_back能更好地避免内存的拷贝与移动,使容器插入元素的性能得到进一步提升。在大多数情况下应该优先使用emplace_back来代替push_back。所有的标准库容器( array除外,因为它的度不可改变,不能插人元素)都增加了类似的方法:emplace、emplace_hint、emplace_frontemplace_after和 emplace_back,关于它们的具体用法可以参考cppreference.com。

#include <vector>
#include <map>
#include <string>
#include <iostream>

using namespace std;

struct Complicated
{
	int year;
	double country;
	string name;

	Complicated(int a, double b, string c) : year(a), country(b), name(c)
	{
		cout << "is constructed" << endl;
	}

	Complicated(const Complicated& other) : year(other.year),
		country(other.country), name(other.name)
	{
		cout << "is moved" << endl;
	}
};

int main(void)
{
	map<int, Complicated> m;
	int anInt = 4;
	double aDouble = 5.0;
	string aString = "C++";

	cout << "--insert--" << endl;
	m.insert(make_pair(4, Complicated(anInt, aDouble, aString)));

	cout << "--emplace--" << endl;
	m.emplace(4, Complicated(anInt, aDouble, aString));

	cout << "--emplace_back--" << endl;
	vector<Complicated> v;
	v.emplace_back(anInt, aDouble, aString);

	cout << "--push_back--" << endl;
	v.push_back(Complicated(anInt, aDouble, aString));

	system("pause");
	return 0;
}

输出如下:

--insert--
is constucted
is moved
is moved
--emplace--
is constucted
is moved
--emplace_back--
is constucted
--push_back--
is constucted
is moved
is moved

用map的 insert方法插入元素时有两次内存移动,而用emplace时只有一次内存移动;用vector的 push_back插入元素时有两次移动内存,而用emplace_back时没有内存移动,是直接构造的。
可以看到, emplace/emplace_back 的性能比之前的insert和 push_back 的性能要提高很多,我们应该尽量用emplace/emplace_back 来代替原来的插入元素的接口以提高性能。需要注意的是,我们还不能完全用emplace_back 来取代push_back 等老接口,因为在某些场景下并不能直接使用emplace来进行就地构造,比如,当结构体中没有提供相应的构造函数时就不能用emplace了,这时就只能用push_back。

5 unordered container无序容器

C++11增加了无序容器unordered_map/unordered_multimap和 unordered_set/unorderedmultiset,由于这些容器中的元素是不排序的,因此,比有序容器map/multimap和 set/multiset效率更高。map和 set 内部是红黑树,在插入元素时会自动排序,而无序容器内部是散列表(Hash Table),通过哈希(Hash),而不是排序来快速操作元素,使得效率更高。由于无序容器内部是散列表,因此无序容器的key需要提供hash_value函数,其他用法和 map/set的用法是一样的。不过对于自定义的key,需要提供Hash函数和比较函数。

#include <iostream>
#include <vector>
#include <bitset>
#include <string>
#include <utility>
#include <unordered_map>

struct Key
{
	std::string first;
	std::string second;
};

struct KeyHash
{
	std::size_t operator()(const Key& k) const
	{
		return std::hash<std::string>()(k.first) ^
			(std::hash<std::string>()(k.second) << 1);
	}
};

struct KeyEqual
{
	bool operator()(const Key& lhs, const Key& rhs) const
	{
		return lhs.first == rhs.first && lhs.second == rhs.second;
	}
};

int main(void)
{
	std::unordered_map<std::string, std::string> m1;
	std::unordered_map<int, std::string> m2 = { { 1, "foo" }, { 2, "bar" }, { 3, "baz" } };
	std::unordered_map<int, std::string> m3 = m2;
	std::unordered_map<int, std::string> m4 = std::move(m2);

	std::vector<std::pair<std::bitset<8>, int>> v = { { 0x12, 1 }, { 0x01, -1 } };
	std::unordered_map<std::bitset<8>, double> m5(v.begin(), v.end());

	std::unordered_map<Key, std::string, KeyHash, KeyEqual> m6 =
	{ { { "John", "Doe" }, "example" }, { { "Mary", "Sue" }, "another" } };

	system("pause");
	return 0;
}

对于基本类型来说,不需要提供Hash函数和比较函数,用法上和 map/set一样,对于自定义的结构体,就稍微复杂一些,需要提供函数和比较函数。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C++性能优化 指南(强列推荐) chm版 Part I: Everything But the Code Chapter 1. Optimizing: What Is It All About? Performance Footprint Summary Chapter 2. Creating a New System System Requirements System Design Issues The Development Process Data Processing Methods Summary Chapter 3. Modifying an Existing System Identifying What to Modify Beginning Your Optimization Analyzing Target Areas Performing the Optimizations Summary Part II: Getting Our Hands Dirty Chapter 4. Tools and Languages Tools You Cannot Do Without Optimizing with Help from the Compiler The Language for the Job Summary Chapter 5. Measuring Time and Complexity The Marriage of Theory and Practice System Influences Summary Chapter 6. The Standard C/C++ Variables Variable Base Types Grouping Base Types Summary Chapter 7. Basic Programming Statements Selectors Loops Summary Chapter 8. Functions Invoking Functions Passing Data to Functions Early Returns Functions as Class Methods Summary Chapter 9. Efficient Memory Management Memory Fragmentation Memory Management Resizable Data Structures Summary Chapter 10. Blocks of Data Comparing Blocks of Data The Theory of Sorting Data Sorting Techniques Summary Chapter 11. Storage Structures Arrays Linked Lists Hash Tables Binary Trees Red/Black Trees Summary Chapter 12. Optimizing IO Efficient Screen Output Efficient Binary File IO Efficient Text File IO Summary Chapter 13. Optimizing Your Code Further Arithmetic Operations Operating System–Based Optimizations Summary Part III: Tips and Pitfalls Chapter 14. Tips Tricks Preparing for the Future Chapter 15. Pitfalls Algorithmic Pitfalls Typos that Compile Other Pitfalls
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

给算法爸爸上香

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值