以下问题探究点在64
位操作系统下进行探究。
0x01什么是内存对齐
编译器为程序中的每个数据单元安排在合适的位置上,来实现CPU
以极少的次数获取到内存中的数据。【这是编译器干的活】
上例子:
// C语言示例
typedef struct {
int a; // 4 Byte
double b; // 8 Byte
short c; // 2 Byte
}AStruct;
typedef struct {
short b; // 2 Byte
int a; // 4 Byte
double c; // 8 Byte
}BStruct;
// 测试用例
AStruct a = {1,2.0f,3};
BStruct b = {1,2,3.0f};
NSLog(@"%d-%.2f-%d-size:%d",a.a,a.b,a.c,sizeof(a));
NSLog(@"%d-%d-%.2f-size:%d",b.a,b.b,b.c,sizeof(b));
# TestMemoryAlign[4069:228467] 1-2.00-3-size:24
# TestMemoryAlign[4069:228467] 2-1-3.00-size:16
发现了点什么?同一个结构体,为啥sizeof
查出来的大小不一样呢?
这就是编译器在编译的时候做了内存对齐导致不同的效果。
对于我们来说,内存对齐基本是透明的,这是编译器该干的活,编译器为程序中的每个数据单元安排在合适的位置上,从而导致相同的变量,不同声明顺序的结构体大小不同。
0x02 为什么要内存对齐
#pragma pack(1) # 不使用内存对齐 后面会解释
typedef struct {
int a; // 4 Byte
double b; // 8 Byte
short c; // 2 Byte
}AStruct;
AStruct a = {1,2.0f,3};
# TestMemoryAlign[4136:236686] 1-2.00-3-size:14
-
从存储角度
如果只是根据变量类型分配不同的空间,那么如果按照直接存储,不是可以做到让结构体类型大小就是所有变量大小之和,而非经过对齐增加了内存空间占用。这样不是更好吗? -
从读取角度
如果只是按照将各种类型放到指定为值,那么读取效果为:
# 假设 a从0开始,当然如果a不从0开始会更复杂
a 读取 0-3字节
b 读取 4-11字节
c 读取 12-13字节
从上面看来,似乎没有什么问题,按着这个偏移量去读取指定字节数量的数据不就行了? 这我们就需要考虑内存读取的方式了。
内存的读取
专有名词
-
Chip
: 一个内存是由若干个黑色的内存颗粒构成的。每个内存颗粒叫做Chip
-
Bank
: 每个Chip
内部,是由8
个Bank
组成的。 - 电容:每个
Bank
内部,都是电容的行列(二维)矩阵结构。
二维矩阵中的一个元素一般存储着
8
个Bit
。
8
个(Bank
)相同位置(行列位置)的元素(小电容),一起组成在内存中连续的64
个Bit
。
图例分析

以上为内存条效果,内存条中每一个小黑块就是一个chip
。此内存条有8
个chip
。

以上为Chip
的内部结构,及每次读取8
字节的方式:从每个Bank
的相同位置,各读取一个字节,然后拼接为8
字节的数据。
从上图展示的内存的物理结构我们可以看出:
内存中最小的单位就是小电容,最小就是
1
个字节。所以操作系统管理的时候,最小的单位也是1
个字节。在内存中连续的
64
个Bit
,其实在内存的物理结构中并不连续,而是分散在同位置的8
个Bank
上。
内存对齐的本质
内存硬件设计分为多个Chip
,每个Chip
内部维护这8
个Bank
表,每个Bank
表包含着行列矩阵的电容单元。
内存在进行IO
的时候,一次操作取的就是64bit
。所以内存对齐最底层的原因就是内存的IO
以64bit
为单位进行操作。
所以在64
位CPU
上,我们每次读取是一次8
字节。那么就是:
0-7Byte 一次读取
8-15Byte 一次读取
16-23Byte 一次读取
那么回到我们上面遗留的问题
从上面看来,似乎没有什么问题,按着这个偏移量去读取指定字节数量的数据不就行了?
从这里我们可以发现,当我们访问一个变量的时候,最好的方式就是CPU
一次能够读取出需要的数据。那么我们看上面假设的读取方式,能否一次读取需要的数据。
a 读取 0-3字节 0-7Byte 一次读取可成功
b 读取 4-11字节 0-7Byte 8-15Byte 需要两次读取才可以获取到
c 读取 12-13字节 8-15Byte 一次读取可成功
从上面分析可以发现,我们读取b
时需要内存工作两次才能正确读取。
这还是a
从0
开始的时候,那么如果a
不是从0
开始,那我们就需要CPU
操作内存更多次才能获取需要的信息。
在数据量比较大的情况下这无疑是一个巨大的消耗。以下为验证消耗的用例:
#pragma pack(1) # 不使用内存对齐
typedef struct {
short a; // 2 Byte 出现一个不足8字节,后面但凡是超出8字节的全部都是一次内存读取不成功的
long b; // 8 字节
int c; // 4 Byte
double d; // 8 Byte
int64_t e;
long f;
long g;
}AStruct;
#pragma pack(8)
typedef struct {
short a; // 2 Byte
long b; // 8 字节
int c; // 4 Byte
double d; // 8 Byte
int64_t e;
long f;
long g;
}BStruct;
static int maxCount = 1000000000;
void testUnAlign() {
AStruct a = {1,200000000,3,100.0f,1000000000,10012032,324324234};
NSLog(@"%d-%ld-%d-%.2f-%d-%ld-size:%d",a.a,a.b,a.c,a.d,a.e,a.f,sizeof(a));
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < maxCount; i++) {
AStruct a = {1,200000000,3,100.0f,1000000000,10012032,324324234};
int64_t sum = a.a + a.b + a.c + a.d + a.e + a.f + a.g;
}
CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
NSLog(@"未对齐耗时:%f",end - start);
}
void testAlign() {
BStruct a = {1,200000000,3,100.0f,1000000000,10012032,324324234};
NSLog(@"%d-%ld-%d-%.2f-%d-%ld-size:%d",a.a,a.b,a.c,a.d,a.e,a.f,sizeof(a));
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < maxCount; i++) {
BStruct a = {1,200000000,3,100.0f,1000000000,10012032,324324234};
int64_t sum = a.a + a.b + a.c + a.d + a.e + a.f + a.g;
}
CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
NSLog(@"对齐耗时:%f",end - start);
}
// 测试效果
testUnAlign();
testAlign();
# TestMemoryAlign[4805:290147] 1-200000000-3-100.00-1000000000-10012032-size:46
# TestMemoryAlign[4805:290147] 未对齐耗时:15.066716
# TestMemoryAlign[4805:290147] 1-200000000-3-100.00-1000000000-10012032-size:56
# TestMemoryAlign[4805:290147] 对齐耗时:5.225200
经由测试发现,结构体内变量越多,遍历次数maxCount
越多,未对齐查找操作耗时就越多。
由此可以确定,内存对齐对程序性能的影响是极大的。
内存对齐的作用
为什么要内存对齐
- 平台因素
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能再某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能问题
通过上测试用例可发现
- 内存对齐优点
经过内存对齐后,
CPU
的内存访问速度大大提升
- 内存对齐缺点
内存对齐会消耗一部分存储空间
0x03 内存对齐规则
对齐规则
内存对齐的规则【参考】:
- 结构体变量的起始地址能够被其最宽的成员大小整除
- 结构体每个成员相对于起始地址的偏移能够被其自身大小整除,如果不能则在前一个成员变量之后补充字节
- 结构体总体大小能够被最宽的成员的大小整除,如果不能则在后面补充字节。
上面的规则其实并不完善,还有内存对齐系数我们是可以指定的,所以还需要和下面配合处理内存对齐。
// x为对齐系数,对齐系数应该为多少 1,2,4,8,16 ; 1为不进行内存对齐
#pragma pack(x)
对齐规则整理如下:
- 字节对齐
对于结构体各个成员,第一个成员位于偏移为0的位置,长度为y字节,对齐系数为x
a.下一个偏移量为 min(x,y) 的正整数倍;
b.下一个变量的偏移量/变量大小 等于正整数。
- 整体对齐
所有对齐后,结构体(或共用体)本身也要进行对齐,假设结构体(或共用体)内部最大数据成员长度为maxLength
那么结构体(或联合)整体大小为:min(x,maxLength) 的正整数倍
栗子
// #pragma pack(8) // 64位机默认是8
typedef struct {
int a; // 4 Byte 0-3Byte nextOffset = min(8,4)*倍数 8:默认对齐系数 4:前变量长度
double b; // 8 Byte 7-15Byte 这个大小为8Byte,那么偏移量就要是8字节的倍数,同时还要是min(8,4)的倍数,那么偏移量为8即可,
short c; // 2 Byte 16-17Byte 大小2Byte
}AStruct;
// 整体计算变量存储使用18Byte,但是整体还要进行对齐,最大变量长度为8字节,要是8的正整数倍,最小就是24Byte,所以结果就是24Byte
// 打印
AStruct a = {1,2.0f,3};
NSLog(@"%d-%.2f-%d-size:%d",a.a,a.b,a.c,sizeof(a));
# TestMemoryAlign[4907:301999] 1-2.00-3-size:24
// 我们还可以查看下内存结构确认是否是我们分析的那样
(lldb) p/x &a
(AStruct *) $0 = 0x00007ffeefbff508
(lldb) x/4g 0x00007ffeefbff508
0x7ffeefbff508: 0x0000000000000001 0x4000000000000000 # 1占了4个字节,然后填充4个字节0,然后是8字节的b
0x7ffeefbff518: 0x0000000000000003 0x00007ffeefbff558 # 3 占了后面两个字节,填充了6个字节的0,再后面的8个字节就不属于AStruct了
0x04 内存对齐使用
C
语言结构体共用体优化
通过上述操作,我们使用了内存对齐,但是耗费内存空间的大小可以通过调整成员变量的顺序来控制。
iOS
是否需要优化
iOS
在分配内存时已经调用了对齐操作
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize(); // 返回对齐后的内存地址
}
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() const { // 直接获取所有变量的内存大小
ASSERT(isRealized());
return data()->ro->instanceSize;
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16; // 如果分配的内存大小小于16,那么就为16
return size;
}
总结
内存对齐对与我们开发者来说,基本是透明的,了解内部结构有助于我们的理解,深究则与硬件的组成有关联,有助于我们对内存知识的进一步了解。
扩展
如果不强制对地址进行操作,仅仅只是简单用
C
定义一个结构体,编译和链接器会自动替开发者对齐内存的。尽量帮你保证一个变量不跨列寻址。
其实在内存硬件层上,还有操作系统层。操作系统还管理了
CPU
的一级、二级、三级缓存。实际中不一定每次
IO
都从内存出,如果你的数据局部性足够好,那么很有可能只需要少量的内存IO
,大部分都是更为高效的高速缓存IO
。但是高速缓存和内存一样,也是要考虑对齐的。
网友评论