优化动态分配内存的变量

在C++中,每个变量在内存中的布局都是固定的,而且,每个变量都有它的生命周期,只有在这段时间内,变量所占用的内存中的值才是有意义的,为变量分配内存的开销取决于存储期

静态存储期

在程序编译的时候,编译器会为每一个静态变量在预留的内存空间中分配一个固定位置和大小的内存空间

静态变量的生命周期等同于程序的生命周期,只有在main函数退出的时候才会被销毁

因为是在编译器进行内存分配,所以在运行期没有开销

被声明为static/extern的变量具有静态存储期

线程局部存储期

C++11开始,程序可以声明具有线程局部存储期的变量,它们在线程开始的时候被构建,在线程结束的时候被销毁,每一个线程都包含一个独立的副本,例如如下例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <thread>
#include <iostream>
thread_local int i = 0;
void func()
{
for (int j = 0; j < 10; j++)
{
std::cout << std::this_thread::get_id() << ":" << ++i << std::endl;
}
}
int main()
{
std::thread t1(func);
std::thread t2(func);
t1.join();
t2.join();
return 0;
}

由于将全局变量声明为thread_local,因此线程开始的时候,对于i,每个线程都会有一份独立的副本

thread_local变量的存储空间是线程分配的,相比直接访问同一个全局变量,访问thread_local的开销更大

自动存储期

就是在函数/代码块中定义的一般变量,不会产生运行时的开销

动态存储期

这些变量会保存在程序申请的内存中,由内存管理器显式主动请求存储空间并构造(new),然后显式地释放(delete),而且这些变量会产生显著的运行期开销

值对象和实体对象

值对象也就是右值,实体对象也就是左值

例如

1
int i = 34;

34就是值对象,i就是实体对象,值对象是临时的,当语句结束就会被销毁,而实体对象则是持久的

动态相关

  • 指针和引用
  • new \ new [] & delete \ delete []
  • 类构造和析构函数
  • 智能指针
  • 分配器模版

智能指针

由于手工管理动态变量十分困难,因此可以采用智能指针进行自动管理

智能指针可以正确地在合适的位置销毁动态变量,其销毁时间根据变量的生命周期而定

值得注意的是,类似shared_ptr这种采用引用计数的智能指针,由于在引用计数上会发生昂贵的原子性加减运算,因此shared_ptr所产生的开销更大,但是正因为如此,shared_ptr可以在多线程上工作

动态变量的运行期开销

大多数C++语句产生的只是几次内存访问,但是一旦涉及到动态分配内存,内存访问的次数则会达到数千次,为什么会有那么显著的差距呢?一切的罪魁祸首就是C++的内存管理器,在C++所提供的内存管理机制中,会使用较为复杂的内存池,因此构造和析构都会产生昂贵的运行期开销

减少动态变量的使用

  • 静态地创建实例,如果情况允许,尽量使用MyClass object(argument list)而不是MyClass *object_ptr = new MyClass(argument list)
  • 使用两段初始化
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
class MyString
{
private:
std::string str;
public:
MyString(std::string &t) : str(t){}
MyString(){}
void setString(std::string &t)
{
str = t;
}
};

class Test
{
private:
MyString *p;
MyString p2;
int i;
public:
Test(int _i)
{
i = _i;
}
//Bad Practice
void setString(std::string& str)
{
p = new MyString(str); //为MyString调用了内存管理器,大大增加了开销
}
//Best Practice
void setString2(std::string& str)
{
p2.setString(str); //并没有为MyString调用内存管理器
}
};

上面这个就是一个简单的例子,也就是能不用new尽量不用,每删掉一次内存管理器的调用,都会提升性能

使用静态的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
TimeTEST(vector_test)
{
std::vector<int> k;
for (int i = 0; i < 1000000; i++)
{
k.emplace_back(i); //动态地拓展容量
}
} //use 38ms

TimeTEST(vector_test2)
{
std::vector<int> k(1000000); //事先拓展容量
for (int i = 0; i < 1000000; i++)
{
k[i] = i;
}
}//use 15ms

TimeTEST(array_test)
{
std::array<int, 1000000> k; //在编译期已经分配好容量
for (int i = 0; i < 1000000; i++)
k[i] = i;
}//use 3ms

从上面的测试可以看出,静态的数据结构性能实在太过优秀,很多时候,我们可能只是为了顺手,使用一些动态的容器来完成一些数组完全足以搞定的工作,然后就造成了性能上的极度浪费