C/C++中的数据结构对齐
总览
数据结构对齐是指在计算机内存中排列和访问数据的方式。它包含三个独立但相关的问题:数据对齐(data alignment),数据结构填充( data structure padding)和打包(packing)。
当数据自然对齐时,现代计算机硬件中的CPU最有效地执行对内存的读写操作,这通常意味着数据的内存地址是数据大小的倍数。
- 读/写总是从word_size的倍数的地址开始的。
- 读/写的长度总是word_size的倍数。 例如,在32位体系结构中,如果数据存储在4个连续字节中并且第一个字节位于4字节边界上,则可以对齐数据。
数据对齐是根据元素的自然对齐来进行的。为了确保自然对齐,可能需要在结构元素之间或结构的最后一个元素之后插入一些填充。例如,在32位机器上,一个包含16位值和32位值的数据结构可以在16位值和32位值之间有16位的填充,以使32位值在32位边界对齐。或者,我们可以将结构打包(packing),省略填充,这可能会导致较慢的访问速度,但只使用了四分之三的内存。
尽管数据结构对齐是所有现代计算机的一个基本问题,但许多计算机语言和计算机语言实现都会自动处理数据对齐。Fortran、Ada、 PL/I、Pascal、某些C和C++实现、D、Rust、C#和汇编语言,允许至少在部分范围内控制数据结构填充,这在某些特殊情况下可能是有用的。
数据对齐(alignment)
当一个内存地址a是n的倍数(其中 n = 2 k n=2^k n=2k)时,可以说是n字节对齐的。在这种情况下,一个字节是最小的内存访问单位,即每个内存地址指定一个不同的字节。一个n字节对齐地址,再用二进制表示时将至少有 l o g 2 ( n ) log_2(n) log2(n)个最低有效零。
而b位对齐,实际上就是另一种说法,指 b / 8 b/8 b/8字节对齐的地址(例如,64位对齐的是8字节对齐的)。
当被访问的数据长度为n个字节,并且基准地址是n个字节对齐的时候,就可以说一个内存访问是对齐的。当一个内存访问没有被对齐时,它被称为错位(misaligned)。请注意,根据定义,字节内存访问总是被对齐的。
如果一个内存指针指的是n字节长的原始数据,并且只允许它包含n字节对齐的地址,那么这个指针就被称为对齐的。否则,它就被称作不对齐的(unaligned)。当(且仅当)集合中的每个原始数据是对齐的,指向数据集合(数据结构或数组)的内存指针才是对齐的。
请注意,上面的定义假定每个原始数据的长度是 2 k 2^k 2k。如果不是这种情况(如x86的80位浮点),上下文会影响数据是否被认为是对齐的条件。
数据结构可以存储在堆栈中,在栈中的静态尺寸被称为 有界(bounded),在堆中的动态尺寸被称为 无界(unbounded)。
典型对齐方式
结构的典型对齐方式:
-
数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
-
联合 :按其包含的长度最大的数据类型对齐。
-
结构体: 结构体中每个数据类型都要对齐。
结构字节对齐的原则主要有:
-
数据类型自身的对齐值:char型数据自身对齐值为1字节,short型数据为2字节,int/float型为4字节,double型为8字节 …(操作系统不同可能由偏差),在最后会有介绍。
-
结构体或类的自身对齐值:其成员中自身对齐值最大的那个值。(结构体的每一个成员相对结构体首地址的偏移量,应该其参数类型占字节数的整数倍,如果不满足则补足前面的字节使其满足),例如:
typedef struct node2 { int a; char b; short c; } S2;
补位结构为==(4 — 1 + 1(补) — 2)==,因为最后一个
short
占 2 2 2字节,但其前一个char
占 1 1 1位,不补位其地址较结构体首地址偏移 5 ≠ 2 ∗ N 5 \ne 2*{\rm{N}} 5=2∗N,所以前面char
补 1 1 1位,首地址偏移 6 = 2 ∗ N 6=2*N 6=2∗N ,总大小为 8 8 8字节,具体地址如下:这其实就是数据结构填充的补齐方式,详细例子在后面的填充中会说明。
-
指定对齐值:
#pragma pack (value)
时的指定对齐值value。 -
数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack值}。
不对齐存在的问题
CPU一次访问内存一个内存字。只要内存字大小至少与计算机支持的最大基本数据类型一样大,对齐访问将始终访问一个内存字。然而,对于未对齐的数据访问可能就不是这样了。
如果一个数据的高位和低位字节不在同一个内存字中,计算机必须将该数据的访问分成多个内存访问。 这需要复杂的电路来生成内存访问并协调它们。为了处理内存字位于不同的内存页的情况,处理器必须在执行指令之前验证两个内存页是否都存在,或者在任何内存访问过程中能够处理TLB缺失或页面故障。
内存地址不对齐,会引起什么样的问题呢?用一个例子来说明:
假定在一台32位机器上,有一个整型变量i的地址是34 ,那i存储在内存的34、35、36、37地址;
为了把这个变量从内存读进CPU,由于计算机从内存读取数据的天性(第一点,读/写总是从word_size的倍数的地址开始的),需要两次读取(第一次从32开始读32 33 34 35,第二次从36开始读36 37 38 39),然后把第一次读取的后两个字节(34 35)抽取出来,把第二次读取的前面两个字节(36 37)抽取出来拼到一起组成变量i:
一个int变量为4bytes,即32位,从CPU一次可以读取的内存块长度来看,本可以一次读完;但是因为这个变量的内存块地址没有对齐,将导致本来一个read指令就能完成的读取操作,需要两次read外加其它复杂的抽取拼接计算,从而大大地降低了性能。
一些处理器设计有意避免引入这种复杂性,在不对齐的内存访问时会产生替代行为。 例如,在ARMv6 ISA之前的一些ARM架构实现需要所有多字节的加载和存储指令强制对齐内存访问。根据执行哪个具体的指令,尝试未对齐访问的结果可能是将违规地址的最低有效位舍入为对齐访问(有时附加其他警告),或者抛出MMU异常(如果有MMU硬件),或者无声地产生其他潜在不可预测的结果。ARMv6和更新的架构在许多情况下支持不对齐访问,但并非全部情况。
当单个内存字被访问时,操作是原子性的,即整个内存字被一次性读或写,其他设备必须等到读或写操作完成后才能访问它。这对于对多个内存字的非对齐访问来说可能不是正确的,例如,第一个字由一个设备读取,然后另一设备写入两个字,然后再由第一设备读取第二个字。这种情况读取的值既不是原始值也不是更新的值。虽然这种故障很少,但却很难识别。
解决对齐:数据结构填充(padding)
尽管编译器(或解释器)通常将单个数据项分配到对齐的边界上,但数据结构通常具有不同对齐要求的成员。为了保持适当的对齐,翻译器通常插入额外的未命名数据成员,以便每个成员都得到适当的对齐。此外,整个数据结构可能会用最后一个未命名成员填充。这使得结构数组的每个成员都可以得到适当的对齐。
仅当结构成员后面紧跟具有较大对齐要求的成员或位于结构末尾时,才插入填充。通过改变结构中成员的顺序,可以改变维护对齐所需的填充量。例如,如果成员按降序排列要求进行排序,则需要最少的填充。所需的最少填充量始终小于结构中最大的对齐要求。计算所需最大填充量更为复杂,但始终小于所有成员对齐要求之和减去最不对齐的一半结构成员对齐要求之和的两倍。
尽管 C 和 C++ 不允许编译器重新排序结构成员以节省空间,但其他语言可能允许。大多数 C 和 C++ 编译器也可以指定将结构体成员“打包”到特定的对齐级别,例如 “pack(2)”表示将大于一个字节的数据成员对齐到两个字节边界,使得任何填充成员最多为一个字节长。同样,在 PL/I 中,可以声明结构体为 UNALIGNED 以除去除位串外的所有填充。
这种 "打包 " 结构的 一个用途是节约内存。例如,一个包含一个字节(如char)和一个四字节整数(如uint32_t)的结构将需要三个额外的字节填充。一个由此类结构组成的大数组如果被打包,将减少37.5%的内存,尽管访问每个结构可能需要更长时间。这种妥协可以被认为是一种空间-时间权衡的形式。
尽管使用 "打包 "结构最常被用来节省内存空间,但它也可以被用来 格式化数据结构,以便使用标准协议进行传输。然而,在这种用法中,还必须注意确保结构成员的值是以协议要求的字节数(通常是网络字节数)来存储的,这可能与主机本身使用的字节数不同。
计算填充
下面的公式提供了对准数据结构的开始所需的填充字节数(其中mod是模除运算符):
padding
=
(
align
−
(
offset
m
o
d
align
)
)
m
o
d
a
l
i
g
n
aligned
=
offset
+
padding
=
offset
+
(
(
align
−
(
offset
m
o
d
align
)
)
m
o
d
a
l
i
g
n
)
\begin{aligned} \text { padding } & =(\text { align }-(\text { offset } \ mod \ \text{ align })) \ mod \text \ { align } \\ \text { aligned } & =\text { offset }+ \text { padding } \\ & =\text { offset }+((\text { align }-(\text { offset } \ mod \ \text{ align })) \ mod \text \ { align }) \end{aligned}
padding aligned =( align −( offset mod align )) mod align= offset + padding = offset +(( align −( offset mod align )) mod align)
例如,对于一个地址偏移是0x59d
的4字节对齐结构体,该结构体将从0x5a0
开始存储并添加3字节的填充,因为0x5a0
是4的倍数。计算过程如下:
padding
=
(
4
−
(
0x59d
m
o
d
4
)
)
m
o
d
4
=
(
4
−
1
)
m
o
d
4
=
3
aligned
=
0x59d
+
3
=
0x5a0
\begin{aligned} \text { padding } &= (4 - (\text{0x59d} \ mod \ 4)) \ mod \ 4 = (4 - 1) \ mod \ 4 = 3 \\ \text { aligned } &= \text{0x59d} + 3 = \text{0x5a0} \end{aligned}
padding aligned =(4−(0x59d mod 4)) mod 4=(4−1) mod 4=3=0x59d+3=0x5a0
反之,当地址偏移量offset
已经和对齐字节数align
相等时,第二个*(align - (offset mod align)) mod align*中的模除将返回0,因此原始值将保持不变。
因为对齐操作根据定义是按照2的幂次方,所以模除运算可以简化为布尔和位运算。
下面的公式可以产生正确的值(其中&是位与,~是位非)—— 但前提是地址偏移量是无符号的,或者系统使用二进制补码运算:
padding = ( align − ( offset & ( align − 1 ) ) ) & ( align − 1 ) = − offset & ( align − 1 ) aligned = ( offset + ( align − 1 ) ) & ∼ ( align − 1 ) = ( offset + ( align − 1 ) ) & − a l i g n \begin{aligned} \text { padding } & =(\text { align }-(\text { offset \& }(\text { align }-1))) \&(\operatorname{align}-1) \\ & =- \text { offset \& }(\text { align }-1) \\ \text { aligned } & =(\text { offset }+(\text { align }-1)) \& \sim(\text { align }-1) \\ & =(\text { offset }+(\text { align }-1)) \&-a l i g n \end{aligned} padding aligned =( align −( offset & ( align −1)))&(align−1)=− offset & ( align −1)=( offset +( align −1))&∼( align −1)=( offset +( align −1))&−align
数据对齐的编译器处理
编译器尝试以防止数据未对齐的方式分配数据。对于简单的数据类型,编译器将分配是数据类型的大小(以字节为单位)的倍数的地址。 例如,编译器将地址分配给类型为 long
且是 4 的倍数的变量,并将地址的最底部 2 位设置为零。
编译器还以自然对齐结构的每一个元素的方式填充结构。 来看看下面代码示例中的结构 struct x_
:
struct x_
{
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
char d; // 1 byte
} bar[3];
编译器填充此结构以自动强制实施对齐方式。
下面的代码示例展示了编译器如何将填充的结构置于内存中:
// Shows the actual memory layout
struct x_
{
char a; // 1 byte
char _pad0[3]; // padding to put 'b' on 4-byte boundary
int b; // 4 bytes
short c; // 2 bytes
char d; // 1 byte
char _pad1[1]; // padding to make sizeof(x_) multiple of 4
} bar[3];
两个声明都将 sizeof(struct x_)
作为 12 个字节返回。
第二个声明包括两个填充元素:
char _pad0[3]
,对齐 4 字节边界上的int b
成员,为了int b
地址偏移为 4 ∗ ( s i z e o f ( i n t ) ) 4*(sizeof(int)) 4∗(sizeof(int)),补3字节。char _pad1[1]
,对齐 4 字节边界上结构struct _x bar[3]
的数组元素,为了数组中下一个struct _x bar
地址偏移为 4 ∗ N 4*N 4∗N,补1字节。
填充以允许自然访问的方式对齐 bar[3]
的元素。
下面的代码示例展示了 bar[3]
的数组布局:
adr offset element
------ -------
0x0000 char a; // bar[0]
0x0001 char pad0[3];
0x0004 int b;
0x0008 short c;
0x000a char d;
0x000b char _pad1[1];
0x000c char a; // bar[1]
0x000d char _pad0[3];
0x0010 int b;
0x0014 short c;
0x0016 char d;
0x0017 char _pad1[1];
0x0018 char a; // bar[2]
0x0019 char _pad0[3];
0x001c int b;
0x0020 short c;
0x0022 char d;
0x0023 char _pad1[1];
结构体填充举例
如果有如下八个结构体:
typedef struct node1 {
} S1;
typedef struct node2 {
int a;
char b;
short c;
} S2;
typedef struct node3 {
char a;
int b;
short c;
} S3;
typedef struct node4 {
int a;
short b;
long c;
} S4;
typedef struct node5 {
char a;
S1 b;
short c;
} S5;
typedef struct node6 {
char a;
S2 b;
int c;
} S6;
typedef struct node7 {
char a;
S2 b;
double d;
int c;
} S7;
typedef struct node8 {
char a;
S2 b;
char* c;
} S8;
int main() {
S1 xx;
printf("start:\t 0x%p \n", &xx);
printf("end:\t 0x%p \n", &xx+1);
return 0;
}
按照之前的公式,在未指定对齐的情况下,各个结构体占用的内存如下:
node1
:
node1
为一个空结构体,在C中空结构体的大小为0字节,在C++中空结构体的大小为1字节。
typedef struct node1 { // C:0, C++:1
} S1;
node2
:
node2
的内存结构:(4 — 1 + 1(补) — 2),总大小为8字节(结构体的每一个成员相对结构体首地址的偏移量应该是对其参数的整数倍)。
typedef struct node2 {
// 系数 当前地址偏移 下一系数 padding 最终空间
int a; // 4 4 % 1 =0 => 补0 4
char b; // 1 (1+4) % 2 =1 => 补2-1=1 1 + 1(补)
short c; // 2 (2+2+4) % 4 =0 => 补0 2
// 最后一个元素的下一系数参考本结构体最大元素的系数
} S2;
node3
:
node3
的内存结构:(1 +3(补) — 4 — 2 +2(补)),总大小为12字节(结构体的每一个成员相对结构体首地址的偏移量应该是对其参数的整数倍)。
typedef struct node3 {
// 系数 当前地址偏移 下一系数 padding 最终空间
char a; // 1 1 % 4 =1 => 补4-1=3 1 +3(补)
int b; // 4 (4+4) % 2 =0 => 补0 4
short c; // 2 (2+4+4) % 4 =2 => 补4-2=2 2 +2(补)
// 最后一个元素的下一系数参考本结构体最大元素的系数
} S3;
node4
:
node4
的内存结构:(4 — 2 + 2(补)—4),总大小为12字节。
typedef struct node4 {
// 系数 当前地址偏移 下一系数 padding 最终空间
int a; // 4 4 % 2 =0 => 补0 4
short b; // 2 (2+4) % 4 =2 => 补4-2=2 2 + 2(补)
long c; // 4 (4+4+4) % 4 =0 => 补0 4
// 最后一个元素的下一系数参考本结构体最大元素的系数
} S4;
node5
:
node5
的内存结构:(1 — 1 — 2),总大小为4字节。
typedef struct node5 {
// 系数 当前地址偏移 下一系数 padding 最终空间
char a; // 1 1 % 0 ->0 => 补0 1
S1 b; // 0 (0+1) % 2 =1 => 补1 1
short c; // 2 (2+1+1) % 4 =0 => 补0 2
// 最后一个元素的下一系数参考本结构体最大元素的系数
} S5;
node6
:
node6
的内存结构:(1 + 3(补) — 8 — 4),总大小为16字节,注意结构体变量的对齐参数的计算。
这里
char
补3的原因是:S2
结构体内基本数据类型占空间最大的是4字节int
类型 ,所以要补足4个字节。
typedef struct node6 {
// 系数 当前地址偏移 下一系数 padding 最终空间
char a; // 1 1 % 4 =1 => 补4-1=3 1 + 3(补)
S2 b; // 4 (8+4) % 4 =0 => 补0 8
int c; // 4 (4+8+4) % 4 =0 => 补0 4
// 最后一个元素的下一系数参考本结构体最大元素的系数
} S6;
node7
:
node7
的内存结构:(1 + 3(补)— 8 + 4(补) — 8 — 4 + 4(补)),总大小为32字节。
typedef struct node7 {
// 系数 当前地址偏移 下一系数 padding 最终空间
char a; // 1 1 % 4 = 1 => 补4-1=3 1 + 3(补)
S2 b; // 4 (8+4) % 8 = 4 => 补8-4=4 8 + 4(补)
double d; // 8 (8+12+4) % 4 = 0 => 补0 8
int c; // 4 (4+8+12+4) % 8 = 4 => 补8-4=4 4 + 4(补)
// 最后一个元素的下一系数参考本结构体最大元素的系数
} S7;
node8
:
node8
的内存结构:(1 + 3(补) — 8 + 4(补)— 8),所有数据类型指针在64位系统都占8字节,总大小为24字节。
typedef struct node8 {
// 系数 当前地址偏移 下一系数 padding 最终空间
char a; // 1 1 % 4 = 1 => 补4-1=3 1 + 3(补)
S2 b; // 4 (8+4) % 8 = 4 => 补8-4=4 8 + 4(补)
char* c; // 8 (8+12+4) % 8 = 0 => 补0 8
// 最后一个元素的下一系数参考本结构体最大元素的系数
} S8;
C语言结构体在x86上的典型对齐方式
数据结构成员在内存中是按顺序存储的,因此,在下面的结构中,成员Data1总是在Data2之前;而Data2总是在Data3之前:
struct MyData
{
short Data1;
short Data2;
short Data3;
};
如果 "short "类型存储在2字节的内存中,那么上面描述的数据结构的每个成员都将是2字节对齐的。数据1在偏移0,数据2在偏移2,数据3在偏移4。这个结构体的大小将是6个字节。
结构中每个成员的类型通常有一个默认的对齐方式,也就是说,除非程序员另有要求,否则它将在一个预先确定的边界上对齐。以下典型的对齐方式对微软(Visual C++)、Borland/CodeGear(C++Builder)、Digital Mars(DMC)和GNU(GCC)的编译器在为32位x86编译时有效:
数据类型 | 对齐字节数 |
---|---|
char(1字节) | 1字节对齐 |
short(2字节) | 2字节对齐 |
int(4字节) | 4字节对齐 |
long(4字节) | 4字节对齐 |
float(4字节) | 4字节对齐 |
double(8字节) | Windows上是8字节对齐的,在Linux上是4字节对齐的(用-malign-double编译时选项是8字节) |
long long(8字节) | 4字节对齐 |
long double((C++Builder和DMC为10个字节,Visual C++为8个字节,GCC为12个字节)) | C++Builder上将是8字节对齐,DMC为2字节对齐,Visual C++为8字节对齐,GCC为4字节对齐。 |
指针(4字节) (例如: char* , int* ) | 4字节对齐 |
long(8字节) | 8字节对齐 |
double(8字节) | 8字节对齐 |
long long(8字节) | 8字节对齐 |
long double(在Visual C++中是8个字节,在GCC中是16个字节) | 在Visual C++中是8字节对齐的,在GCC中是16字节对齐的。 |
指针(8字节) | 8字节对齐 |
有些数据类型则取决于实现方式。
强制指定对齐大小
在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:
使用伪指令#pragma pack (n)
,C编译器将**按照n个字节对齐**。
使用伪指令#pragma pack ()
,取消自定义字节对齐方式。
gcc里还可以使用__attribute__
关键字来声明数据类型的对齐方式,优先级高于#pragma
预编译指令。
__attribute((aligned (n)))
,让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。
attribute ((packed))
,取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。
__attribute__方式
struct stu{
char sex;
int length;
char name[10];
}__attribute__ ((aligned (1)));
struct stu my_stu;
此时,sizeof(my_stu)
可以得到大小为15。上面的定义等同于:
struct stu{
char sex;
int length;
char name[10];
}__attribute__ ((packed));
struct stu my_stu;
-
当aligned作用于**变量时,其作用是告诉编译器为变量分配内存的时候,要分配在指定对其的内存上,作用于变量之上不会改变变量的大小**。
例如:int a attribute((aligned(16))),该变量a的内存起始地址为16的倍数。
-
当aligned作用于**类型时,其作用是告诉编译器该类型声明的所有变量都要分配在指定对齐的内存上。当该属性作用于结构体声明时可能会改变结构体的大小**。
测试的时候发现 __attribute__ ((aligned (1)))
用法不适用于minGW
,使用第二种,效果如下:
struct node6 {
char a;
S2 b;
int c;
}__attribute__ ((packed)) ;
typedef struct node6 S6;
可以发现与之前的声明相比,内存分配发生了变化,变量分配在了1对齐的内存上:
pragma方式
声明#pragma
可以设置对齐参数的数值,缺省是8字节:
#pragma pack(n) //作用:C编译器将按照n个字节对齐。
#pragma pack() //作用:取消自定义字节对齐方式。
#pragma pack(push,1) //作用:是指把原来对齐方式设置压栈,并设新的对齐方式设置为一个字节对齐
#pragma pack(pop) //作用:恢复对齐状态
#pragma pack(push) //保存对齐状态
#pragma pack(4) //设定为4字节对齐 相当于 #pragma pack (push,4)
例如之前的node6
,使用强制指定对齐的pack
方式,实际上就是指定系数均为2:
# pragma pack(2)
typedef struct node6 {
// 系数 当前地址偏移 下一系数 padding 最终空间
char a; // 2 1 % 2 =1 => 补2-1=1 1 + 1(补)
S2 b; // 2 (8+2) % 2 =0 => 补0 8
int c; // 2 (4+8+2) % 2 =0 => 补0 4
// 最后一个元素的下一系数参考本结构体最大元素的系数
} S6;
#pragma pack()
可以看到运行结果符合对齐要求,占用空间从之前的16字节缩小到了14字节:
aligned和pack的主要区别
- pack作用于结构体或类的定义,而**aligned既可以作用于结构体或类的定义,也可以作用于变量的声明**。
- pack的作用是改变结构体或类中成员变量的布局规则,而aligned只是建议编译器对指定变量或指定类型的变量分配内存时的规则。
- pack可以压缩变量所占内存的空间
- align可以指定变量在内存的对其规则,而pack不可以。
- 若某一个结构体的默认pack为n,pack指定的对齐规则m大于n,则该pack忽略。若aligned指定的对齐规则s大于n,则此时结构体的大小一定为s的整数倍。
- aligned和pack指定规则时都必须为2的n次幂。
拓展:64 位数据模型
在 32 位程序中,指针和整数等数据类型通常具有相同的长度。但在 64 位机器上不一定如此。因此,在C及其后代(如C++和Objective-C)等编程语言中混合数据类型可能适用于 32 位实现,但不适用于 64 位实现。
在 64 位机器上的许多 C 和 C 派生语言的编程环境中,int
变量仍然是 32 位宽,但long int
和pointer
是 64 位宽。这些被描述为具有LP64 数据模型,它是==“Long, Pointer, 64”== 的缩写。其他模型有 ILP64数据模型,其中所有三种数据类型都是64位宽,甚至还有SILP64模型,其中short
也是64位宽。然而,在大多数情况下,所需的修改相对较小且直接,许多编写良好的程序可以简单地针对新环境重新编译而无需更改。另一种选择是LLP64模型,它通过将int
和long
保留为 32 位来保持与 32 位代码的兼容性。LL指的是long long integer类型,在所有平台上至少是64位,包括32位环境。
也有使用ILP32数据模型的 64 位处理器的系统,添加了 64 位 long long
整数;这也用于许多具有 32 位处理器的平台。该模型以更小的地址空间为代价减少了代码大小和包含指针的数据结构的大小,对于某些嵌入式系统来说是一个不错的选择。对于 x86 和 ARM 等指令集,其中 64 位版本的指令集比 32 位版本的指令集具有更多的寄存器,它提供了对额外寄存器的访问而没有空间损失。它在 64 位 RISC 机器中很常见,在 x86 中作为x32 ABI 进行了探索,并且最近被用于Apple Watch Series 4和 5。
Data model | short (integer) | int | long (integer) | long long | pointers, size_t | Sample operating systems |
---|---|---|---|---|---|---|
ILP32 | 16 | 32 | 32 | 64 | 32 | x32 and arm64ilp32 ABIs on Linux systems; MIPS N32 ABI. |
LLP64 | 16 | 32 | 32 | 64 | 64 | Microsoft Windows (x86-64 and IA-64) using Visual C++; and MinGW |
LP64 | 16 | 32 | 64 | 64 | 64 | Most Unix and Unix-like systems, e.g., Solaris, Linux, BSD, macOS. Windows when using Cygwin; z/OS |
ILP64 | 16 | 64 | 64 | 64 | 64 | HAL Computer Systems port of Solaris to the SPARC64 |
SILP64 | 64 | 64 | 64 | 64 | 64 | Classic UNICOS[46][47] (versus UNICOS/mp, etc.) |
今天许多64位平台使用LP64模型
(包括Solaris、AIX、HP-UX、Linux、macOS、BSD和IBM z/OS)。微软Windows使用LLP64模型
。LP64模型
的缺点是,将long
存储到int
中会被截断。另一方面,在LP64中,将一个指针转换为一个长字符串会 “有效”。在LLP64模型
中,情况恰恰相反。这些问题并不影响完全符合标准的代码,但代码的编写往往隐含着对数据类型宽度的假设。C代码在将指针转换为整数对象时,应该选择(u)intptr_t
而不是long
。
参考文献
1:Data structure alignment - Wikipedia
2:数据结构对齐 - 维基百科,自由的百科全书
3:64-bit computing - Wikipedia
4:Data structure alignment (数据结构对齐 / 内存对齐)_Jeff_的博客-CSDN博客
5:对齐方式 | Microsoft Learn
如有疑问或错误,欢迎和我私信交流指正。
版权所有,未经授权,请勿转载!
Copyright © 2023 by Mr.Idleman. All rights reserved.