C++内存对齐
真正的高手总是精益求精,不会放过任何一个能够优化的机会。
一. 首先是32位和64位系统的类型所占字节数。
数据类型 | 说明 | 32位字节数 | 64位字节数 | 取值范围 |
---|---|---|---|---|
bool | 布尔型 | 1 | 1 | true,false |
char | 字符型 | 1 | 1 | -128~127 |
unsigned char | 无符号字符型 | 1 | 1 | 0~255 |
short | 短整型 | 2 | 2 | -32768~32767 |
unsigned short | 无符号短整型 | 2 | 2 | 0~65535 |
int | 整型 | 4 | 4 | -2147483648~2147483647 |
unsigned int | 无符号整型 | 4 | 4 | 0~4294967295 |
long | 长整型 | 4 | 8 | – |
unsigned long | 无符号长整型 | 4 | 8 | – |
long long | 长整型 | 8 | 8 | -2^64~2^64-1 |
float | 单精度浮点数 | 4 | 4 | 范围-2^128~2^128 精度为6~7位有效数字 |
double | 双精度浮点数 | 8 | 8 | 范围-2^1024~2^1024 精度为15~16位 |
long double | 扩展精度浮点数 | 8 | 8 | 范围-2^1024~2^1024 精度为15~16位 |
* | 指针 | 4 | 8 | – |
除了指针与long随操作系统长变化而变化外,其他的都固定不变(32位和64位相比)
二. 然后就是内存对齐,具体规则是:
1.每个变量按照变量类型的整数倍进行对齐。
2.整个结构体按照结构体中所占字节数最长的变量与机器字中较小的对齐。例如:32位系统机器字为4个字节,结构体中最长的为short占2个字节,那么按照2个字节对齐;如果结构体中最长的为double,那么按照4个字节对齐。
可以参考下面例子理解一下(64位操作系统):
1 struct MyStruct 2 { 3 4 }; 5 int main() 6 { 7 cout << sizeof(MyStruct); //1 8 return 0; 9 }
1 struct MyStruct 2 { 3 double i = 0; 4 5 }; 6 int main() 7 { 8 cout << sizeof(MyStruct); //8 9 return 0; 10 }
1 struct MyStruct 2 { 3 int i = 0; 4 5 }; 6 int main() 7 { 8 cout << sizeof(MyStruct); //4 9 return 0; 10 }
1 struct MyStruct 2 { 3 char a; //0 4 int b ; //4-7 5 bool c; //8 6 double d; //16-23 7 }; 8 int main() 9 { 10 cout << sizeof(MyStruct); //24 11 return 0; 12 }
1 struct MyStruct 2 { 3 char a; //0 4 int b ; //4-7 5 bool c; //8 6 int d[4]; //12-27 7 }; 8 int main() 9 { 10 cout << sizeof(MyStruct); //28 11 return 0; 12 }
1 struct A 2 { 3 char a; //0 4 int b; //4-7 5 bool c; //8 6 int d[4]; //12-27 7 }; 8 struct MyStruct 9 { 10 char a; //0 11 int b ; //4-7 12 bool c; //8 13 A d; //12-39 结构体对齐看规则2 14 }; 15 int main() 16 { 17 cout << sizeof(MyStruct); //40 18 return 0; 19 }
1 struct A 2 { 3 char a; //0 4 int b; //4-7 5 bool c; //8 6 double d; //16-23 7 }; 8 struct MyStruct 9 { 10 char a; //0 11 int b ; //4-7 12 bool c; //8 13 A d; //16-39 结构体对齐看规则2 14 }; 15 int main() 16 { 17 cout << sizeof(MyStruct); //40 18 return 0; 19 }
三. 类的内存对齐
类的内存对齐与结构十分相似,这里单独说是因为类还有函数,继承等与结构体不同的地方。
1.类中的静态成员,函数并不占类的内存,这是因为静态成员和函数是由所有成员所共享的
1 class A 2 { 3 public: 4 A(){}; 5 ~A(){}; 6 void func() 7 { 8 return; 9 } 10 static int i; 11 12 }; 13 int A::i = 0; 14 int main() 15 { 16 cout << sizeof(A); //1 17 return 0; 18 }
2.继承一个空类(无成员变量),类的大小由子类来决定
1 1 class A 2 2 { 3 3 public: 4 4 A(){}; 5 5 ~A(){}; 6 6 void func() 7 7 { 8 8 return; 9 9 } 10 10 static int i; 11 11 12 12 }; 13 13 int A::i = 0; 14 14 15 15 class B 16 16 { 17 17 public: 18 18 B(){}; 19 19 ~B(){}; 20 20 }; 21 21 22 22 23 23 int main() 24 24 { 25 25 cout << sizeof(B); //1 26 26 return 0; 27 27 }
1 class A 2 { 3 public: 4 A(){}; 5 ~A(){}; 6 void func() 7 { 8 return; 9 } 10 static int i; 11 12 }; 13 int A::i = 0; 14 15 class B 16 { 17 public: 18 B(){}; 19 ~B(){}; 20 int i; 21 }; 22 23 24 int main() 25 { 26 cout << sizeof(B); //4 27 return 0; 28 }
3.继承一个非空类,相当于把父类当成一个成员变量,进行对齐
1 class A 2 { 3 public: 4 A(){}; 5 ~A(){}; 6 void func() 7 { 8 return; 9 } 10 static int i; 11 char a; 12 13 }; 14 int A::i = 0; 15 16 class B:A 17 { 18 public: 19 B(){}; 20 ~B(){}; 21 int i; 22 }; 23 24 //继承非空类与将父类当成成员变量相似 25 26 /*class B 27 { 28 public: 29 B(){}; 30 ~B(){}; 31 A a; //0 32 int i; //4-7 33 };*/ 34 35 int main() 36 { 37 cout << sizeof(B); //8 38 return 0; 39 }
1 class A 2 { 3 public: 4 A(){}; 5 ~A(){}; 6 void func() 7 { 8 return; 9 } 10 static int i; 11 char a; 12 double b; 13 }; 14 int A::i = 0; 15 16 class B:A 17 { 18 public: 19 B(){}; 20 ~B(){}; 21 int i; 22 }; 23 24 //继承非空类与将父类当成成员变量相似 25 26 /*class B 27 { 28 public: 29 B(){}; 30 ~B(){}; 31 A a; //0-15 32 int i; //16-20 整个还需要按照最大类型A中double进行8字节对齐 33 };*/ 34 35 int main() 36 { 37 cout << sizeof(A)<<endl; //16 38 cout << sizeof(B); //24 39 return 0; 40 }
4.类中有虚函数会使得类隐式创建一个虚指针指向类的虚表,这个指针在所有成员变量之前,与虚函数声明位置无关
class A { public: A(){}; ~A(){}; static int i; char a; virtual void func() { return; } }; int A::i = 0; int main() { cout << sizeof(A)<<endl; //8 return 0; }
5.虚指针可以继承,子类继承的虚指针会指向子类的虚表,所以子类再定义虚函数不会再次创建虚指针
1 class A 2 { 3 public: 4 A(){}; 5 ~A(){}; 6 7 static int i; 8 char a; 9 virtual void func() 10 { 11 return; 12 } 13 }; 14 int A::i = 0; 15 16 class B:A 17 { 18 public: 19 B(){}; 20 ~B(){}; 21 virtual int f() 22 { 23 return 0; 24 } 25 }; 26 27 int main() 28 { 29 cout << sizeof(A)<<endl; //8 30 cout << sizeof(B); //8 31 return 0; 32 }
四. 内存对齐的原因
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
2.硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。具体原因接下来解释
图一:
我们普通程序员心中的内存印象,由一个个字节组成,但是CPU却不是这么看待的
图二:
cpu把内存当成是一块一块的,块的大小可以是2,4,8,16 个字节,因此CPU在读取内存的时候是一块一块进行读取的,块的大小称为(memory granularity)内存读取粒度。
我们再来看看为什么内存不对齐会影响读取速度?
假设CPU要读取一个4字节大小的数据到寄存器中(假设内存读取粒度是4),分两种情况讨论:
1.数据从0字节开始
2.数据从1字节开始
解析:当数据从0字节开始的时候,直接将0-3四个字节完全读取到寄存器,结算完成了。
当数据从1字节开始的时候,问题很复杂,首先先将前4个字节读到寄存器,并再次读取4-7字节的数据进寄存器,接着把0字节,4,6,7字节的数据剔除,最后合并1,2,3,4字节的数据进寄存器,对一个内存未对齐的寄存器进行了这么多额外操作,大大降低了CPU的性能。
但是这还属于乐观情况,上文提到内存对齐的作用之一是平台的移植原因,因为只有部分CPU肯干,其他部分CPU遇到未对齐边界就直接罢工了。
参考图片: