现在的位置: 首页 > 算法 > 正文

C/C++中为什么要内存对齐

2020年01月15日 算法 ⁄ 共 2008字 ⁄ 字号 评论关闭

  代码分区:在使用C/C++编程时,我们定义的变量存在于内存中,而内存在C语言的角度上可以分为五大区。局部变量在栈区,静态/全局变量在全局区,动态申请的变量存在于堆区,const修饰的变量/字符常量存在于只读区。无论是什么样的变量,终究在内存中。

  CPU取指,译码,执行:存在于内存中的目的是为了CPU通过总线的进行寻址,取指令,译码,执行取数据,内存与寄存器交互,然后CPU运算,再输出数据至内存。这个过程反复的,高速的执行。

  CPU位数:在计算机中,最小的存储单元为字节(Byte),理论上任意地址(比如0x20000002,0x20000003,0x2000011...)都可以通过总线进行访问(CPU寻址),而每次寻址后,传输的数据大小跟CPU位数相关,常见的CPU位数有8位,16位,32位,64位。位数越高,单次操作执行的数据量越大,性能也就越强。

  OS位数:操作系统一般与CPU位数相匹配,32位CPU可以寻址4GB内存空间,可以运行32位的OS。同样,64位的CPU可以运行32位的OS,也可以运行64位的OS。

  Compiler:虽然编译器都是在翻译/编译代码,进行预处理(宏展开,头文件展开),编译(语法检查等),汇编(翻译为机器码),链接(重定位等)这四部分的工作。但是不同的编译器的内部默认设置以及用法会有所差异,常用的有GCC,VS,Clang,MinGW等。

  指定平台

  这里的平台指的是三大件:CPU + OS + Compiler

  本文中实验的平台是:Intel i7 + ubuntu16.04 + gcc5.4

  有了上面的基本概念了解,就可以进行分析了。

  为什么要内存对齐?

  原因有两点:

  CPU每次寻址都是要消费时间的,如果一次取不完数据就要取多次。比如int类型的变量a占4Byte,假设在内存中没有对齐(所谓对齐,指的是内存中数据的首地址是CPU单次获取数据大小的整数倍),且存放在0x00000003 - 0x00000006处(0x00000003不是4的整数倍)。那么每次取4字节(32位宽总线)的CPU第一次取到[0x00000000 - 0x00000003],只得到变量a的1/4数据,进而需要进行第二次取数[0x00000004 - 0x00000007],为了得到int类型的一个变量,却需要两次访问内存,并且还需要拼接处理,性能较低,这是其一。

  有些CPU(ARM架构的)在内存非对齐的情况下,执行二进制代码会崩溃,因为不是所有的硬件平台都能访问任意地址上的任意数据的。倘若代码移植到其他不支持的平台上,不具有可移植性,这是其二。

  若在编译时,将分配的内存进行对齐,单次访问内存就可以获取数据,并且具有平台可移植性。

  那谁来把我们编写的结构体,类中的成员变量进行对齐呢?

  当然是编译器(Compiler)。那对齐的规则又是如何呢?

  内存对齐规则

  编译器提供手动指定对齐值的关键字 #pragma pack(N),可以手动设置对齐的字节数,比如#pragma pack(1),#pragma pack(4)等。这里即为N。

  若没有手动指定,那么编译器就会默认将成员变量中最大的类型字节数设置为对齐值:m

  1、整体对齐值:

  首先计算对齐单位 n = min{N,m},然后整体对齐后的字节数应该为n的倍数,不够的在最后面填补占位。

  2、成员对齐值:

  首个成员的偏置地址(offset) = 0。

  假定该成员的类型占字节数 j,那么本成员的偏移地址(offset):min{n, j}的整数倍。

  代码实测

  #include < stdio.h>

  #include < iostream>

  using namespace std;

  class Test

  {

  public:

  char c; // offset = 0x20000000. 区间:[0x20000000,0x2000000]

  int i; //offset = min{8,4}的整数倍为0x20000004. 区间:[0x20000004,0x20000007]

  short s; //offset = min{8,2}的整数倍,0x20000008. 区间:[0x20000008,0x200009]

  double d; //offset = min{8,8}的整数倍,0x20000010. 区间:[0x20000010,0x200017]

  //整体占24字节,并且24 为 min{8,8}的整数倍,故对齐,无需在尾部填充占位。

  Test(){}

  ~Test(){}

  void fff(){}

  };

  class A

  {

  int i;

  char c1;

  };

  class B: public A

  {

  char c2;

  };

  class C: public B

  {

  char c3;

  };

  int main()

  {

  cout << sizeof(char) << endl; // 1   cout << sizeof(short) << endl; // 2   cout << sizeof(int) << endl; // 4   cout << sizeof(long) << endl; // 8   cout << sizeof(double) << endl; // 8   cout << "sizeof C: " << sizeof(C) << endl; //先继承,后对齐。相当于对一个大的类进行对齐   Test test;   cout << "Test class: " << sizeof(Test) << " " << sizeof(test) << endl;   return 0;   }   以上注释为理论分析。   现在编译,并执行输出看看是否sizeof = 24。   这里使用的GCC中的g++进行编译。   也可以用gcc,不过要链接c++的标准库(-lstdc++),否则会链接失败。   这里实验结果与理论分析的一致。   另外:如果手动设置#pragma pack(4),后效果如何呢?   #pragma pack(4)   class Test   {   public:   char c; // offset = 0x20000000. 区间:[0x20000000,0x2000000]   int i; //offset = min{4,4}的整数倍为0x20000004. 区间:[0x20000004,0x20000007]   short s; //offset = min{4,2}的整数倍,0x20000008. 区间:[0x20000008,0x200009]   double d; //offset = min{4,8}的整数倍,0x2000000C. 区间:[0x2000000C,0x200013]   //整体占20字节,并且20 为 min{4,8}的整数倍,故对齐,无需在尾部填充占位。   Test(){}   ~Test(){}   void fff(){}   };   #pragma pack()   查看结果是否为sizeof = 20呢?   显然,是和分析的一致。   总结

  这里以C++的类为例,进行内存对齐分析。关于C++的内存布局,以及含有virtual函数的类,实际上还会更复杂。简单的就如最基本的类,这和C中的struct是非常类似的。

  掌握C++中类的内存对齐,有助于进一步理解C++对象模型。

抱歉!评论已关闭.