本文主要对内存对齐规则、作用以及其对结构体的影响进行阐述。
从例子开始
先来看一个例子:
1 |
|
输出是12.
1 |
|
输出是8.
我们可以看到,类 test1 和 test2 的成员变量完全一样,只是定义顺序不一样,却造成了 2 个类占用内存大小不一样。这就是编译器内存对齐的缘故。
内存对齐规则
内存对齐规则如下:
- 对于类(或结构或联合,下同)的各个数据成员,第一个成员位于偏移为 0 的位置,以后每个成员的偏移量必须是 min(#pragma pack()指定的数,这个数据成员的自身长度) 的倍数。
- 在数据成员完成各自对齐之后,类本身也要进行对齐,对齐将按照 #pragma pack 指定的数值和类最大数据成员长度中,比较小的那个进行。
很明显 #pragma pack(n) 作为一个预编译指令用来设置多少个字节对齐的。值得注意的是,n 的缺省数值是按照编译器自身设置,默认为 8。其语法如下:
其中:
例子分析
对于类 test1 的内存空间
内存分配过程:
- char 和编译器默认的内存缺省分割大小比较,char 比较小,分配一个字节给它。
- int 和编译器默认的内存缺省分割大小比较,int 比较小,占 4 字节。只能空3个字节,重新分配 4 个字节。
- short 和编译器默认的内存缺省分割大小比较,short 比较小,占 2 个字节,分配 2 个字节给它。
- 对齐结束类本身也要对齐,所以最后空余的 2 个字节也被 test1 占用。
对于类 test2 的内存空间
内存分配过程:
- int 和编译器默认的内存缺省分割大小比较,int比较小,占 4 字节。分配 4 个字节给int。
- char 和编译器默认的内存缺省分割大小比较,char 比较小,分配一个字节给它。
- short 和编译器默认的内存缺省分割大小比较,short 比较小,此时前面的 char 分配完毕还余下 3 个字节,足够 short 的 2 个字节存储,所以 short 紧挨着。分配 2 个字节给 short。
- 对齐结束类本身也要对齐,所以最后空余的 1 个字节也被 test2 占用。
**使用 #pragma pack(n) **
1 |
|
输出结果:
可以看到,当我们把编译器的内存分割大小设置为 1 后,类中所有的成员变量都紧密的连续分布。
内存对齐的原因
要严重参考一 IBM 的文章:Data alignment: Straighten up and fly right,PDF版本可从这里下载得到。
博文内存对齐的规则以及作用说明了内存对齐的原因:
平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因:经过内存对齐后,CPU的内存访问速度大大提升。
该博文还对 IBM 的这篇文章进行了部分翻译(这里进行了重新组织)。
- 普通程序员心目中的内存印象,由一个个的字节组成:
图一
- 可CPU并不是这么看待的。CPU 把内存当成是一块一块的,块的大小可以是 2,4,8,16 字节大小,因此 CPU 在读取内存时是一块一块进行读取的。块大小成为 memory access granularity(粒度),可以把它翻译为“内存读取粒度” 。如图二所示:
图二
假设内存读取粒度为 4,CPU 要读取一个 int 型 4 字节大小的数据到寄存器中,分两种情况讨论:
1)数据从0字节开始
2)数据从1字节开始
- 当该数据是从 0 字节开始时,很 CPU 只需读取内存一次即可把这 4 字节的数据完全读取到寄存器中。如下图三左边图所示:
图三
- 当该数据是从 1 字节开始时,问题变的有些复杂,此时该 int 型数据不是位于内存读取边界上,也就是内存未对齐的数据。读取过程如下图四(及上图三右边图)所示:
图四
此时 CPU 先访问一次内存,读取 03 字节的数据进寄存器,并再次读取 47 字节的数据进寄存器,接着把第 0 字节和第 5、6、7 字节的数据剔除,最后合并第 1、2、3、4 字节的数据进寄存器。对一个内存未对齐的数据进行了这么多额外的操作,大大降低了CPU性能。
内存对齐对结构体成员变量访问影响
先看下边一小程序:
1 |
|
上边程序中第 1618 和第 2123 行输出的结果是一样的。但如果我们考虑到字节填充的问题时,采用 pstr 那种访问方式就不大对了。所以要采用 ptr 那种访问方式。