iOS 内存对齐

1,442 阅读7分钟

为什么要进行内存对齐

1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

内存对齐规则

1、结构体第一个成员的偏移量是0,以后每个成员相对于结构体首地址的偏移量都是该成员大小有效对齐值(假设为n)中较小那个的整数倍; 简单来说就是min(n, 该成员大小)的整数倍

2、结构体的大小为有效对齐值(假设为n)的整数倍,如有需要,编译器会在最后一个成员之后填充字节。简单来说就是 n的整数倍

3、如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍偏移量地址开始存储。

有效对齐值:
1、当未明确指定时,以结构体中最长的成员的长度为其有效。(自然对齐)
2、使用#pragma pack(n)指定对齐系数为n时,以n和结构体中最长的成员的长度中较小者为其值。(强制对齐)
3、一般编译器都会设置默认对齐系数。

举 🌰 分析

文中测试环境下编译器默认对齐系数为8

Struct T1为例

code
struct T1 {
    double a;
    char b; 
    int c; 
    short d; 
} t1;

size_t s = typeof(t1);
printf("%zu", s); 
// 打印结果为24
成员内存分布

分析

1、未指定内存对齐系数,当前编译器默认系数为8。
2、double a占用8字节, 从偏移量0开始存储,内存分布0x00 ~ 0x07
3、char b 占用1字节,从偏移量8开始存储,内存分布0x08
4、int c 占用4字节,按照内存对齐规则1offset = min(8, 4) 的整数倍 = 4的整数倍,偏移量9不符合,向后面继续寻址,直到偏移量12。则c从偏移量12开始存储,内存分布0x0c ~ 0x10
5、short d 占用2字节,从偏移量16开始存储,内存分布0x11 ~ 0x12
6、参照规则2size = 8的整数倍。虽然t1在内存分布上占18字节,但不满足规则。而满足8的公倍数且大于18的最小数值为24,所以t1的大小为24字节。

使用xcodeView Memory验证
typedef struct T {
    double a;
    char b;
    int c;
    short d;
}T, * P_T;

T1改变为T2

code
struct T2 {
    double a;  
    int b;     
    char c;    
    short d;   
} t2;
size_t s = typeof(t2);
printf("%zu", s); 
// 打印结果为16
成员内存分布

分析

1、未指定内存对齐系数,当前编译器默认系数为8。
2、double a 占用8字节,从偏移量0开始存储,内存分布0x00 ~ 0x07
3、int b 占用4字节,从偏移量8开始存储,内存分布0x08 ~ 0x0b
4、char c 占用1字节,从偏移量12开始存储,内存分布0x12
5、short d 占用2字节,按照内存对齐规则1offset = min(8, 2)的整数倍 = 2的整数倍,偏移量13不满足,继续向后寻址,直到偏移量14。则short d从偏移量14开始存储,内存分布0x0e ~ 0x10
6、t2在内存分布上占用16个字节,满足规则2t2大小为16字节。

struct T2struct T1基本相似,不再做验证

小结: 对比t1t2,在结构相同的情况下,因为存在内存对齐,成员排列顺序不同,结构体所占内存大小也不同,所以在设计结构体时,需考虑内存对齐对大小的影响。

结构体做成员

typedef struct inner {
    short in_a;
    int in_b;
}Inner, * P_Inner;

typedef struct outter{
    char a;
    Inner b;
    char c;
    long d;
    char e;
}Outter, * P_Outter;

size_t s = typeof(Outter);
printf("%zu", s); 
// 打印结果为32
成员内存分布

分析

1、未指定内存对齐系数,当前编译器默认系数为8。
2、char a 占用1字节,从偏移量0开始存储,内存分布0x00
3、Inner b共占用8字节,按照内存对齐规则3offset = max(Inner各成员大小)的整数倍 = 4的整数倍,偏移量1不符合,继续向后寻址,直到偏移量4, 那么Inner b从偏移量4开始存储,内存分布0x04 ~ 0x0b
4、short in_aint in_b参考上面struct T1分析。
5、char c 占用字节1,从偏移量12开始存储,内存分布0x0c
6、long d 占用字节8, 参考内存对齐规则1offset = max(8, 8)的整数倍 = 8的整数倍,偏移量13不符合,向后寻址,直到偏移量16,那么long d从偏移量16开始存储,内存分布0x11 ~ 0x18
7、char e 占用字节1,从偏移量24开始存储,内存分布0x19
8、参照规则2size = 8的整数倍。虽然Outter在内存分布上占25字节,但不满足规则。而满足8的公倍数且大于25的最小数值为32,所以Outter的大小为32字节;

View Memory验证

设置内存对齐系数

使用预编译指令 #pragma pack(n)设置对齐系数,使用#pragma pack()取消已设置对齐系数

不设置对齐系数,当前编译器默认系数为8
typedef struct p {
    char a;
    int b;
    char c;
    long d;
}P;

printf("%zu \n", sizeof(P)); // 打印结果:24
设置对齐系数为1
#pragma pack(1)
typedef struct p1 {
    char a;
    int b;
    char c;
    long d;
}P1;
#pragma pack()

printf("%zu \n", sizeof(P1)); // 打印结果:14
设置对齐系数为2
#pragma pack(2)
typedef struct p2 {
    char a;
    int b;
    char c;
    long d;
}P2;
#pragma pack()

printf("%zu \n", sizeof(P2)); // 打印结果:16
设置对齐系数为4
#pragma pack(4)
typedef struct p3 {
    char a;
    int b;
    char c;
    long d;
}P3;
#pragma pack()

printf("%zu \n", sizeof(P3)); // 打印结果:20
设置对齐系数为8
#pragma pack(8)
typedef struct p4 {
    char a;
    int b;
    char c;
    long d;
}P4;
#pragma pack()

printf("%zu \n", sizeof(P4)); // 打印结果:24

1字节对齐,也可以使用编译器指令__attribute__((packed))

typedef struct p5 {
    char a;
    int b;
    char c;
    long d;
} __attribute__((packed)) P5;

print("%zu \n", sizeof(P5)); // 打印结果:14

上述几个结构体就不具体分析内存结构了,参考上面分析即可。

__attribute __((aligned(n)))

此属性指定了指定类型的变量的最小对齐(假设为n,n必须是大于0的2的次方值),如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。

n > 最大成员长度

typedef struct T {
    long a;
    char b;
    int c;
    char d;
} __attribute__((aligned(16))) T, *P_T;

printf("%zu \n", sizeof(T)); // 打印结果:32

有效对齐值为16

n > 最大成员长度,并使用#pragma pack(n)指定对齐系数

#pragma pack(4)
typedef struct T {
    long a;
    char b;
    int c;
    char d;
} __attribute__((aligned(16))) T, *P_T;
#pragma pack()

printf("%zu \n", sizeof(T)); // 打印结果:32

#pragma pack(4)未生效,有效对齐值为16

n < 某个成员长度

typedef struct T {
    long a;
    char b;
    int c;
    char d;
} __attribute__((aligned(4))) T, *P_T;

printf("%zu \n", sizeof(T)); // 打印结果:24

有效对齐值为最大成员long a长度8

n < 某个成员长度,并使用#pragma pack(n)指定对齐系数

#pragma pack(4)
typedef struct T {
    long a;
    char b;
    int c;
    char d;
} __attribute__((aligned(4))) T, *P_T;
#pragma pack()

printf("%zu \n", sizeof(T)); // 打印结果:20

#pragma pack(4)生效,有效对齐值为4

附:Xcode编译时对象的优化

举个🌰,在main.m中我们写了如下这么一个类

@interface MyObject: NSObject

@property (nonatomic, assign) int i1;
@property (nonatomic, assign) double d;
@property (nonatomic, assign) int i2;

@end

@implementation MyObject

@end

终端输入命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp得到c++文件。
在文件中搜索MyObject_IMPL,可以找到下面结构体

struct MyObject_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _i1;
	int _i2;
	double _d;
};

这里可以看到MyObject类转换成MyObject_IMPL结构体后,对成员变量的结构进行了调整。
如果按照MyObject的结构直接转换,应该是下面的代码:

struct MyObject_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _i1;
	double _d;
	int _i2;
};

当前编译器默认的内存对齐系数为8,那么这个结构体的sizeof的大小为32。而通过命令得到的main-arm64.cpp文件中的结构体sizeof的大小为24。可以看出xcode在将类转化为结构体时会对结构体成员进行优化。