企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] # 字节对齐 在用sizeof计算结构体占用的空间的时候,不是简单把所有元素相加的,这里涉及到内存字节对齐的问题 从理论上来讲,对于任何变量都可以从任何地址访问,但是事实不是如此,实际上访问特定类型的变量只能在特定的地址访问,这就需要各个变量在空间上按照一定的规则排列而不是简单的顺序排列,这就是内存对齐 ![](https://box.kancloud.cn/33f8218998f2cc48d28ab2639601da8f_1313x304.png) cpu虽然每次读取的单位是字节,但是一次是读取一个块,所以有些硬件设备不允许放在内存的奇数位上 内存对齐是操作系统为了提高访问内存的策略.操作系统在访问内存的时候,每次读取一定长度(这个长度是操作系统的对齐数,或者默认对齐数的整数倍).如果没有对齐,为了访问一个变量可能产生二次访问 * 提高存取数据的速度.比如有的平台每次都是从偶地址读取数据,对于一个int地址的变量.若从偶地址单元处存放,则需一个读取周期即可读取该变量,但是若从奇地址单元处存放,则需要2个读取周期读取变量 * 某些平台只能在特定的地址访问特定类型的数据,否则抛出硬件异常给操作系统 ## 如何内存对齐 * 对于标准数据类型,它的地址只要是它的长度的整数倍 * 对于非标准类型,比如结构体,要遵循一下对齐原则 1. 数组成员对齐规则.第一个数组成员应该放在offset为0的地方,以后每个数组成员应该在offset为min(当前成员的大小, #paragama pack(n))整数倍的地方开始(比如int在32位机器为4字节, #paragam pack(2),那么从2的倍数地方开始存储) 2. 结构体总的大小,也就是sizeof的结果,必须是min(结构体内部最大成员, #paragama pack(n))的整数倍,不足要补齐 3. 结构体作为成员的对齐规则,如果一个结构体B里嵌套了另一个结构体A,还是以最大成员类型的大小对齐,但是结构体A的起点为A内部最大成员的整数倍地方(struct B里有struct A,A里有char,int,double等成员,那A应该从8的整数倍开始存储),结构体A中的成员对齐规则仍满足原则1和原则2 ## 手动对齐模式 ~~~ #pragma pack(show) 显示当前packing alignment的字节数,以warning message的形式被显示 #pragma pack(push) 将当前指定的packing alignment数组进行压栈操作,这里的栈是the internal compiler stack,同时设置当前的packing alignment为n,如果n没有指定,则将当前的packing alignment数组压栈 #pragma pack(pop) 从internal compiler stack中删除最顶端的reaord,如果没有指定n,则当前栈顶record即为新的packing alignement数值.如果指定了n,则n成为新的packing alignment值 #pragma pack(n) 指定packing的数值,以字节为单位,缺省数值是8,合法数值分别是1,2,4,8,16 ~~~ ## 为何需要对齐 计算机内存是以字节(Byte)为单位划分的,理论上CPU可以访问任意编号的字节,但实际情况并非如此。 CPU 通过地址总线来访问内存,一次能处理几个字节的数据,就命令地址总线读取几个字节的数据。32 位的 CPU 一次可以处理4个字节的数据,那么每次就从内存读取4个字节的数据;少了浪费主频,多了没有用。64位的处理器也是这个道理,每次读取8个字节。 以32位的CPU为例,实际寻址的步长为4个字节,也就是只对编号为 4 的倍数的内存寻址,例如 0、4、8、12、1000 等,而不会对编号为 1、3、11、1001 的内存寻址。如下图所示: ![](https://box.kancloud.cn/db212cd6247c38402d384c87290877c8_500x87.png) 样做可以以最快的速度寻址:不遗漏一个字节,也不重复对一个字节寻址。 对于程序来说,一个变量最好位于一个寻址步长的范围内,这样一次就可以读取到变量的值;如果跨步长存储,就需要读取两次,然后再拼接数据,效率显然降低了。 例如一个 int 类型的数据,如果地址为 8,那么很好办,对编号为 8 的内存寻址一次就可以。如果编号为 10,就比较麻烦,CPU需要先对编号为 8 的内存寻址,读取4个字节,得到该数据的前半部分,然后再对编号为 12 的内存寻址,读取4个字节,得到该数据的后半部分,再将这两部分拼接起来,才能取得数据的值。 将一个数据尽量放在一个步长之内,避免跨步长存储,这称为内存对齐。在32位编译模式下,默认以4字节对齐;在64位编译模式下,默认以8字节对齐。 为了提高存取效率,编译器会自动进行内存对齐 ~~~ #include <stdio.h> #include <stdlib.h> struct{ int a; char b; int c; }t={ 10, 'C', 20 }; int main(){ printf("length: %d\n", sizeof(t)); printf("&a: %X\n&b: %X\n&c: %X\n", &t.a, &t.b, &t.c); system("pause"); return 0; } ~~~ 在32位编译模式下的运行结果: ~~~ length: 12 &a: B69030 &b: B69034 &c: B69038 ~~~ 如果不考虑内存对齐,结构体变量 t 所占内存应该为 4+1+4 = 9 个字节。考虑到内存对齐,虽然成员 b 只占用1个字节,但它所在的寻址步长内还剩下 3 个字节的空间,放不下一个 int 型的变量了,所以要把成员 c 放到下一个寻址步长。剩下的这3个字节,作为内存填充浪费掉了。请看下图: ![](https://box.kancloud.cn/a2c59a3ab589fe3cc21f996425c7c8c9_220x179.png) 编译器之所以要内存对齐,是为了更加高效的存取成员 c,而代价就是浪费了3个字节的空间。 除了结构体,变量也会进行内存对齐,请看下面的代码: ~~~ #include <stdio.h> #include <stdlib.h> int m; char c; int n; int main(){ printf("&m: %X\n&c: %X\n&n: %X\n", &m, &c, &n); system("pause"); return 0; } ~~~ 在VS下运行: ~~~ &m: DE3384 &c: DE338C &n: DE3388 ~~~ 可见它们的地址都是4的整数倍,并相互挨着。 经过笔者测试,对于全局变量,GCC在 Debug 和 Release 模式下都会进行内存对齐,而VS只有在 Release 模式下才会进行对齐。而对于局部变量,GCC和VS都不会进行对齐,不管是Debug模式还是Release模式。 # 内存大小端对齐 我们看到上面的十六进制的63,二进制是0110 0011. 那么二进制数据是 ~~~ 0000 0000 0000 0000 0000 0000 0110 0011 ~~~ 因为int是4个字节,1个字节等于8位. 4位一组. 但是内存是16进制表示每8位对应一个16进制 ~~~ 00 00 00 63 ~~~ 内存大小端对齐 ~~~ 63 00 00 00 ~~~ 数组在内存是连续的 后面是0a,十六进制对应十进制是10 正好是下个元素 如果你想在内存地址中看到下个元素,就把那地址+4,注意是16进制表示,结果就是下个元素了 大端和小端是指数据在内存中的存储模式,它由 CPU 决定: 1. 大端模式(Big-endian)是指将数据的低位(比如 1234 中的 34 就是低位)放在内存的高地址上,而数据的高位(比如 1234 中的 12 就是高位)放在内存的低地址上。这种存储模式有点儿类似于把数据当作字符串顺序处理,地址由小到大增加,而数据从高位往低位存放。 2. 小端模式(Little-endian)是指将数据的低位放在内存的低地址上,而数据的高位放在内存的高地址上。这种存储模式将地址的高低和数据的大小结合起来,高地址存放数值较大的部分,低地址存放数值较小的部分,这和我们的思维习惯是一致,比较容易理解 ## 为什么有大小端模式之分 计算机中的数据是以字节(Byte)为单位存储的,每个字节都有不同的地址。现代 CPU 的位数(可以理解为一次能处理的数据的位数)都超过了 8 位(一个字节),PC机、服务器的 CPU 基本都是 64 位的,嵌入式系统或单片机系统仍然在使用 32 位和 16 位的 CPU。 对于一次能处理多个字节的CPU,必然存在着如何安排多个字节的问题,也就是大端和小端模式。以 int 类型的 0x12345678 为例,它占用 4 个字节,如果是小端模式(Little-endian),那么在内存中的分布情况为(假设从地址 0x 4000 开始存放): ![](https://box.kancloud.cn/8b6dc25beb44da9cceb3378c3bf5e540_294x63.png) 如果是大端模式(Big-endian),那么分布情况正好相反: ![](https://box.kancloud.cn/ca66b028e7a0932b1d800ebe3e50814b_291x63.png) 我们的 PC 机上使用的是 X86 结构的 CPU,它是小端模式;51 单片机是大端模式;很多 ARM、DSP 也是小端模式(部分 ARM 处理器还可以由硬件来选择是大端模式还是小端模式)。 借助共用体,我们可以检测 CPU 是大端模式还是小端模式,请看代码: ~~~ #include <stdio.h> int main(){ union{ int n; char ch; } data; data.n = 0x00000001; //也可以直接写作 data.n = 1; if(data.ch == 1){ printf("Little-endian\n"); }else{ printf("Big-endian\n"); } return 0; } ~~~ 在PC机上的运行结果: Little-endian 共用体的各个成员是共用一段内存的。1 是数据的低位,如果 1 被存储在 data 的低字节,就是小端模式,这个时候 data.ch 的值也是 1。如果 1 被存储在 data 的高字节,就是大端模式,这个时候 data.ch 的值就是 0。