- 内存对齐
- 什么是内存对齐
- 为什么要内存对齐
- 内存对齐的规则
- 结构体中内存对齐
- sizeof
- 无嵌套
- 有嵌套
- iOS中对象内存对齐
- iOS中获取内存大小方式
- class_getInstanceSize()
- malloc_size()
- iOS中内存对齐
- 实际占用内存对齐方式
- 系统分配内存对齐方式
- 问题
- 内存优化
- 总结
- iOS中获取内存大小方式
内存对齐
什么是内存对齐
- 元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每个元素放置到内存中时,它都会认为内存是按照自己的大小(通常它为4或8)来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始,这就是所谓的内存对齐。
为什么要内存对齐
- 有些
CPU
可以访问任意地址上的任意数据,而有些CPU
只能在特定地址访问数据,因此不同硬件平台具有差异性,这样的代码就不具有移植性,如果在编译时,将分配的内存进行对齐,这就具有平台可以移植性了 CPU
每次寻址都是要消费时间的,并且CPU
访问内存时,是以字长(word size
)为单位访问,所以数据结构应该尽可能地在自然边界上对齐,如果访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次访问,内存对齐后可以提升性能。这是一种以空间换时间的方法,目的降低cpu
开销。- 举例:
- 假如没有内存对齐机制,数据可以任意存放,现在一个
int
变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器。这需要做很多工作。 - 现在有了内存对齐的,
int
类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。
- 假如没有内存对齐机制,数据可以任意存放,现在一个
内存对齐的规则
每个特定平台上的编译器都有自己的默认"对齐系数",常用平台默认对齐系数如下:(32位系统对齐系数是4,64位系统对齐系数是8)。这只是默认对齐系数,实际上对齐系数我们是可以修改的,程序员可以通过预编译命令
#pragma pack()
,n = 1, 2, 4, 8, 18来改变这一系数,其中的n就是你要指定的“对齐系数”
。
- 原则一:数据成员对⻬规则
- 结构体(
struct
)或者联合体(union
)的数据成员,第一个数据成员放在offset为0
的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如 int 4字节,那么存储位置可以是0-4-8-12-16-20
依次类推) - 数据成员的对齐规则可以理解为
min(m, n)
的公式, 其中m
表示当前成员的开始位置,n
表示当前成员所需要的位数。如果满足条件m 整除 n
(即m % n == 0
),n 从 m
位置开始存储, 反之继续检查m+1 能否整除 n
, 直到可以整除, 从而就确定了当前成员的开始位置
- 结构体(
- 原则二:数据成员为结构体
- 如果一个结构体里有某些结构体成员,则
该结构体成员
要从其内部最大元素大小的整数倍地址开始存储,比如struct a
里有struct b
,b
里面有char, int, double, short
,那么b应该从8的整数倍地址开始存储,即最大成员为double 8字节
。
- 如果一个结构体里有某些结构体成员,则
- 原则三:结构(或联合)的整体对齐规则
- 在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照
#pragma pack
指定的数值和结构(或联合)最大数据成员长度中,比较小的整数倍 - 比如说:编译器指定按照8字节对齐,然后结构体最大数据成员长度为4,最终按照4字节对齐
- 在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照
结构体中内存对齐
- 根据上面的内存对齐规则,我们探索一下结构体中内存对齐
sizeof
sizeof
是一个操作符
,不是函数- 我们一般使用
sizeof
计算内存大小时,传入的对象主要是数据类型,这个是在编译阶段就会确定大小而不是运行时。sizeof
最终得到的结果是该数据类型占用空间的大小
无嵌套
struct Struct1 {
double a;
char b;
int c;
short d;
}struct1;
struct Struct2 {
double a;
int b;
char c;
short d;
}struct2;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%lu-%lu", sizeof(struct1), sizeof(struct2));
}
return 0;
}
打印结果:内存对齐[8322:58962] 24-16
- 根据结果发现,结构体中包含的变量相同,只是因为位置不一样,但是内存大小不一样
- 根据内存对齐原则,来分析一下结构体中成员的内存占用
Struct1
中:- 变量
a
: 占8
个字节,offset
从0
开始,min(0,8)
, 即0 ~ 7
存放a
- 变量
b
: 占1
个字节,offset
从8
开始,min(8,1)
, 即8
存放d
- 变量
c
: 占4
个字节,offset
从9
开始,min(9,4)
,9 % 4 != 0
,继续往后移动直到找到可以整除4
的位置12
即12 ~ 15
存放b
- 变量
d
: 占2
个字节,offset
从16
开始,min(16,2)
,即16 ~ 17
存放c
- 实际占用18个字节,根据内存对齐原则三,结构体中最大变量
a
占用8字节,所有最终大小必须是8字节的倍数,最终占用24个字节
- 变量
Struct2
中:- 变量
a
: 占8
个字节,offset
从0
开始,min(0,8)
, 即0 ~ 7
存放a
- 变量
b
: 占4
个字节,offset
从8
开始,min(8,4)
, 即8 ~ 11
存放b
- 变量
c
: 占2
个字节,offset
从12
开始,min(12,2)
,即12 ~ 13
存放c
- 变量
d
: 占1
个字节,offset
从14
开始,min(14,1)
,即14
存放d
- 实际占用了15个字节,根据内存对齐原则三,结构体中最大变量
a
占用8字节,所有最终大小必须是8字节的倍数,最终占用16个字节。
- 变量
有嵌套
struct Struct2 {
double a;
int b;
char c;
short d;
}struct2;
struct Struct3 {
double a;
char b;
int c;
short d;
struct Struct2 str;
}struct3;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%lu-%lu", sizeof(struct2), sizeof(struct3));
}
return 0;
}
打印结果:内存对齐[36173:182032] 16-40
Struct3
中:- 变量
a
: 占8
个字节,offset
从0
开始,min(0,8)
, 即0 ~ 7
存放a
- 变量
b
: 占4
个字节,offset
从8
开始,min(8,4)
, 即8 ~ 11
存放b
- 变量
c
: 占2
个字节,offset
从12
开始,min(12,2)
,即12 ~ 13
存放c
- 变量
d
: 占1
个字节,offset
从14
开始,min(14,1)
,即14
存放d
- 变量
str
,由于它是结构体变量,根据内存对齐规则二:结构体成员要从其内部最大元素大小的整数倍地址开始存储。结构体中最大变量占8字节,所以offset
需要从16开始,Struct2
的内存大小是18字节。所以min(16, 18)
,即16-33存放str
- 实际内存大小是34字节,根据内存对齐原则三,结构体中最大变量
a
占用8字节,所有最终大小必须是8字节的倍数,最终占用40个字节
- 变量
iOS中对象内存对齐
iOS中获取内存大小方式
sizeof()
在上一小节已经讲述class_getInstanceSize()
获取的是一个对象实际占用的内存空间malloc_size()
获取的是系统实际给开辟的内存空间大小
- 我们具体看一下后两个实现
class_getInstanceSize()
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
- 内部调用了
alignedInstanceSize()
方法
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
- 我们需要获取未内存对齐大小
unalignedInstanceSize()
// May be unaligned depending on class's ivars.
// 可以根据类的成员变量进行对齐。
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
- 该方法内部是获取类的成员变量大小
- 对获取到的内存大小,进行对齐
#define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
- 此处采用的是8字节对齐,也就是说对象中成员变量按照8字节对齐。
- 具体计算过程如下:假设
x
为8字节- (x + WORD_MASK) = (8 + 7) = 15,用二进制表示:0000 1111
- ~WORD_MASK = ~7,用二进制表示:1111 1000
- 15 & ~7,用二进制表示:0000 1000
- 最终结果8个字节
malloc_size()
- 获取的是系统实际给开辟的内存空间大小,采用了16字节对齐。
iOS中内存对齐
实际占用内存对齐方式
- 通过
class_getInstanceSize()
方法,我们可以看到内部是通过8字节对齐
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
系统分配内存对齐方式
- 调用系统分配的流程可以查看Runtime源码解析-alloc,里面有完整的
alloc
流程 - 这里我们只需要知道,最终系统分配内存时,调用
instanceSize
方法来获取大小。
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
- 这里一般都会进入
fastInstanceSize
方法
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
- 该方法最后调用了
align16()
方法,进行16字节对齐。该align16()
方法和word_align()
内部实现类似,具体如何16字节对齐请参考前面。
问题
- 为什么实际内存对齐才用8字节?
- 在iOS对象中,由于任何类都继承
NSObject
,然后都会有8字节长的isa_t
指针,所以根据内存对齐规则,iOS对象才用了8字节对齐。
- 在iOS对象中,由于任何类都继承
- 为什么系统开辟不采用8字节,而选择16字节?
- 内存容错:对于一个没有任何成员变量的对象来说,它的大小是8字节(
isa_t
指针)。如果多个没有成员变量的对象存在一起,就会出现每个对象isa_ta
指针挨着情况,没有容错性。而才用16字节对齐,之间有8字节的容错性,可有利于对象的扩展。 - 读取速度快:以16字节的方式去读取,可以加快它的读取速度,提高
cpu
利用率。
- 内存容错:对于一个没有任何成员变量的对象来说,它的大小是8字节(
内存优化
-
由于
iOS
中对象的本质就是结构体,那么我们可以说对象的内存对齐就是结构体的内存对齐么?我们通过例子具体来探究一下 -
先定一个
Test
类,然后Test
类里面的成员变量和Struct1
保持一致
@interface Test : NSObject
@property (nonatomic, assign) double a;
@property (nonatomic, assign) char b;
@property (nonatomic, assign) int c;
@property (nonatomic, assign) short d;
@end
@implementation Test
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Test *test = [Test alloc];
test.a = 10.0;
test.b = 'a';
test.c = 12;
test.d = 100;
NSLog(@"struct1 = %lu Test = %lu",sizeof(struct1), class_getInstanceSize([test class]));
}
return 0;
}
打印结果:内存对齐[42277:218736] struct1 = 24 Test = 24
-
在
Test
类中定义的变量和struct2
中顺序是一样的,但是test
对象会多带一个isa_t
变量,该变量占8个字节。打印结果显示它们的大小是一样的,说明test
对象我们定义的变量直占了16个字节。明明和结构体顺序、类型都一样的,为什么对象占用的内存和结构体却不一样。这是因为苹果的内存优化机制 -
我们具体看一下它是如何进行内存优化的:
- 通过
lldb
断点打印可以看出,a
的读取是通过0x4024000000000000,b
的读取是通过0x0061,c
的读取是通过0x0000000c,d
的读取是通过0x0064。我们可以发现char b; int c; short d;
共用了一个8字节空间。 - 苹果采用时间换空间的方式,通过对对象的属性存储顺序进行重排,达到内存优化的目的。
总结
- 为了提高
cpu
的存储效率和安全访问,制定了内存对齐规则。但是也因为内存对齐,从而浪费了很多内存。苹果为了内存优化尽可能降低内存的浪费,使用了属性重排的方式。 - 既保证了存储的效率,又减少了内存的浪费。