5 提高性能及操作硬件的能力
5.1 常量表达式
5.1.1 运行时常量性与编译时常量性
const int i = 3; //运行时常量
constexpr int GetConst() {return 1;}; //编译时常量
5.1.2 常量表达式函数
常量表达式函数的要求:
- 函数体只有单一的return返回语句。(如果有static_assert, using和typedef指令通常也是可以的)
- 函数必须返回值(不能是void函数)
- 在使用前必须已有定义
- return返回语句表达式中不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式。
注意:constexpr不会对函数产生重写。
5.1.3 常量表达式值
//大多数情况下,如下语句没有区别。
//如果i在全局名字空间中,编译器一定会为i产生数据。而对于j,如果不是有代码显示地使用了它的地址,编译器可以选择不为它生成数据,而仅将其当作编译时期的值(枚举值,也是光有名字,没有产生实际数据)
const int i = 1;
constexpr int j = 1;
//标准要求编译时的浮点常量表达式值的精度要至少等于(或者高于)运行时的浮点数常量的精度
constexpr float k = 1323.342;
//对于自定义类型的数据,需要自定义常量构造函数
struct MyType {
constexpr MyType(int x): i(x) {}
int i;
};
constexpr MyType mt = {0};
常量表达式的构造函数使用上的约束:
- 函数体必须为空
- 初始化列表只能由常量表达式赋值
//编译时期的运算
//实际运行的代码没有调用Fibonacci这个函数,数组的值已经被计算好了
constexpr int Fibonacci(int n) {
return (n == 1) ? 1 : ((n == 2) ? 1 : Fibonacci(n - 1) + Fibonacci(n - 2));
}
int main() {
int fib[] = {
Fibonacci(11), Fibonacci(12),
Fibonacci(13), Fibonacci(14),
Fibonacci(15), Fibonacci(16)
};
for (int i : fib) {
cout << i << endl;
}
}
//模版元编程,基于模版的编译时期运算
//该模版类定一个静态变量val,而val的定义方式是递归的。因此模版将会递归地进行推导
template <long num>
struct Fibonacci {
static const long val = Fibonacci<num - 1>::val + Fibonacci<num - 2>::val;
};
template <>
struct Fibonacci<2> {
static const long val = 1;
};
template <>
struct Fibonacci<1> {
static const long val = 1;
};
template <>
struct Fibonacci<0> {
static const long val = 0;
};
int main() {
int fib[] = {
Fibonacci<11>::val, Fibonacci<12>::val,
Fibonacci<13>::val, Fibonacci<14>::val,
Fibonacci<15>::val, Fibonacci<16>::val,
};
for (int i : fib) {
cout << i << endl;
}
}
5.2 变长模版
5.2.1 变长模版:模版参数包和函数参数包
//Elements为模版参数包
template <typename... Elements> class tuple;
//实现tuple模版,通过定义递归的模版偏特化,使得模版参数包在实例化时能够层层展开,直到参数包中的参数逐渐耗尽或达到某个数量的边界为止。
//参数包在基类描述列表中进行展开
template <typename Head, typename... Tail>
class tuple<Head, Tail...> : private tuple<Tail...> {
Head head;
};
template<>
class tuple<> {}; //边界条件
//非类型的模版
template <long... nums> struct Multiply;
//参数包在表达式中进行展开
template<long first, long... last>
struct Multiply<first, last...> {
static const long val = first * Multiply<last...>::val;
};
template<>
struct Multiply<> {
static const long val =1;
};
int main() {
cout << Multiply<2, 3, 4, 5>::val << endl; //120
cout << Multiply<22, 44, 66, 88, 9>::val << endl; //50599296
}
对于变长模版函数,变长的函数参数也可以声明成函数包,如:
//函数参数包必须唯一,且是函数的最后一个参数
template<typename ...T>
void f(T ... args);
5.2.2 变长模板:进阶
标准定义了以下7种参数包可以展开的位置:
- 表达式
- 初始化列表
- 基类描述列表
- 类成员初始化列表
- 模板参数列表
- 通用属性列表
- lambda函数的捕捉列表
实例化T<X,Y>
注意下面两个包扩展的不同
//解包为 class T<X, Y>: private B<X>, private B<Y> {};
template <typename... A>
class T: private B<A>... {};
//解包为 class T<X, Y>: private B<X, Y> {};
template<typename... A>
class T: private B<A...> {}
C++11中,标准引入新操作符"sizeof…", 作用是计算参数包中的参数个数。
template <typename... A>
int Vaargs(A...args) {
int size = sizeof...(A);
switch (size) {
case 0:
}
return size;
}
5.3 原子类型与原子操作
5.3.1 内存模型,顺序一致性与memory_order
实际默认情况下,在C++ 11中的原子类型的变量在线程中总是保持着顺序执行的特性(非原子类型则没有必要,因为不需要在线程间进行同步),这样的特性为“顺序一致”,即代码在线程中运行的顺序与程序员看到的代码顺序一致。
t = 1;
a = t;
b = 2;
上面代码的伪汇编代码为:
1: Loadi reg3, i; #将立即数1放入寄存器reg3
2: Move reg4, reg3; #将reg3的数据放入reg4
3: Store reg4, a; #将寄存器reg4的数据存入内存地址a
4: Loadi reg5, 2; #将立即数2放入寄存器reg5
5: Store reg5, b; #将寄存器reg5中的数据存入内存地址b
通常,指令总是按照1->2->3->4->5顺序执行,即内存模型为强顺序的,这种情况,a的赋值总是在b的赋值之前发生。
这里指令1,2,3和指令4,5运行顺序上毫无影响(使用了不同的寄存器,以及不同的内存地址),一些处理器就可能将指令执行的顺序打乱,按照1->4->2->5->3顺序执行(通常这样的执行顺序都是超标量的流水线,即一个时钟周期里发射多条指令而产生的)。这种顺序(b的赋值在a的赋值前完成),称为弱顺序。
如果要保证指令的顺序,通常需要在汇编指令中加入一条所谓的内存栅栏。即在指令3和4之间加入
7: Sync
该指令迫使已经进入流水线中的指令都完成后,才执行Sync以后的指令。
C++ 11中一共定义了7种memory_order的枚举值
枚举值 | 定义规则 |
---|---|
memory_order_relaxed | 不对执行顺序做任何保证 |
memory_order_acquire | 本线程中,所有后续的读操作必须在本条原子操作完成后执行 |
memory_order_release | 本线程中,所有之前的写操作完成后才能执行本条原子操作 |
memory_order_acq_rel | 同时包含memory_order_acquire和memory_order_release标记 |
memory_order_consume | 本线程中,所有后续的有关原子类型的操作,必须在本条原子操作完成之后执行 |
memory_order_seq_cst | 全部存取都按顺序执行 (C++ 11中都atomic原子操作默认行为) |
- 原子存储操作可以使用
memory_order_relaxed 、memory_order_release、memory_order_seq_cst - 原子读取操作可以使用
memory_order_relaxed、memory_order_consume、memory_order_acquire、memory_order_seq_cst - RMW操作(read-modify-write),即一些需要同时读写的操作
memory_order_relaxed、memory_order_consume、memory_order_acquire、memory_order_release、memory_order_acq_rel、memory_order_seq_cst
//只需要保证a.store先于b.store发生,b.load先于a.load发生
int Thread1(int) {
int t = 1;
a.store(t, memory_order_relaxed);
b.store(2, memory_order_release); //本原子操作前所有的写原子操作必须完成
}
int Thread2(int) {
while (b.load(memory_order_acquire) != 2); //本原子操作必须完成才能执行之后所有的读原子操作
cout << a.load(memory_order_relaxed) << endl; //1
}
5.4 线程局部存储
线程局部存储(TLS,thread local storage),就是拥有线程生命期及线程可见性的变量。
堆空间、静态数据区(从可执行文件角度看,静态数据区对应的是可执行文件的data、bss段的数据,而从c/c++语言层面而言,则对应的是全局/静态变量)是线程间共享的。
//每个线程将拥有独立的errCode
__thread int errCode;
c++11
//int thread_local errCode;
5.5 快速退出:quick_exit与at_quick_exit
- abort函数不会调用任何的析构函数,而exit属于“正常退出”范畴的程序终止,exit函数会正常调用自动变量的析构函数,并且还会调用atexit注册的函数。
- C++ 11中,标准引入了quick_exit函数,该函数并不执行析构函数而只是使程序终止。而quick_exit与exit同属于正常退出。at_quick_exit注册的函数也可以在quick_exit的时候被调用。