一,为什么要内存对齐
在iOS开发过程中,甚至任何一门开发语言,对于内存的资源都是极其宝贵的,不能随意的浪费,所以才会存在栈内存和堆内存的情况,栈内存就是连续的空间,由系统统一分配,而堆内存是离散的,是由程序员手动开辟使用,使用完成在进行系统回收这样的一个过程。
所以在系统为我们分配内存,然后再去读内存中相应数据的时候,如果随意存储,随意的读取相应存储的东西,这样系统的性能会大大降低,速度也是相当缓慢,从而是计算机的CPU相应的消耗过大,一直在查找、读取内存中的东西,基于这样的优化实现,所以才会有内存对齐
的机制,从而使系统读取数据得到最大优化的实现。
两种内存对齐方式
对于iOS开发过程中存在两种系统内存对齐的方式
-
kind1: 16字节对齐
-
kind2: 8字节对齐
eg1: 16字节对齐方式
从objc 源码可以看出,我们系统的alloc 流程是使用的16字节对齐的方式,代码片段如下
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;
}
step into
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);
}
此处的原理我上一篇文章已经介绍过了,里边有很详细得位与计算,此处就不再赘述了。
eg2:
在libmalloc 开源的库里也能查到,
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
zone = runtime_default_zone();
return zone->calloc(zone, num_items, size);
}
step into
static inline malloc_zone_t *
inline_malloc_default_zone(void)
{
_malloc_initialize_once();
// malloc_report(ASL_LEVEL_INFO, "In inline_malloc_default_zone with %d %d\n", malloc_num_zones, malloc_has_debug_zone);
return malloc_zones[0];
}
void *ptr;
size_t slot_key;
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
mag_index_t mag_index = nano_mag_index(nanozone);
nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
最后到 也是核心算法
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
这也是16对齐的一种证明,
eg 8字节对齐
在runtime源码中的 class_getInstanceSize
方法中
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
step into
图片.png
会进入标注的方法,而我们看到此算法和16字节对齐的算法一样,只是16字节的mask 是15 而8字节的WORD_MASK 是7,所以足以证明是8自己对齐,这就是对象创建时候使用的内存对齐机制。好了,字节对齐应该算介绍明白了,接下来就进入今天的主题,结构体内存对齐,
二,结构体内存对齐
-
简单的结构体内存分析
我们定义两个结构体Student 和Teacher: 两个结构体的成员都一样,只是顺序不一样,我们打印相关的内存空间发现结果是不一样的,打印结果如下
struct Student{
long a;
int b;
short c;
char d;
}struct1;
struct Tearcher{
long a;
short b;
int c;
char d;
}struct2;
图片.png
为什么是这样呢,原来这就是iOS系统中内存对齐的作用导致的,接下来我们就详细的分析结果;
在iOS系统中,所有数据类型的内存占用情况是如下:截图来自逻辑教育那个最帅的男人
再根据内存对齐的规则
1:数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第
⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要
从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,
结构体等)的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存
储。 min(当前开始的位置m n) m = 9 n = 4
9 10 11 12
2:结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从
其内部最⼤元素⼤⼩的整数倍地址开始存储.(struct a⾥存有struct b,b
⾥有char,int ,double等元素,那b应该从8的整数倍开始存储.)
3:收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤
成员的整数倍.不⾜的要补⻬。
我们从以上得知,long = 8、 int = 4、short = 2、char = 1。
struct1 的结果分析
- a 的存储结果 是 #0 到 #7,存储8个字节,
- b的内存开始编号为#8 ,存储4个字节,,8 又正好是4 的整数倍 , 所以 b的存储结果是#8 到 #11,
- c 的内存开始编号是 #12,存储2个字节,12又正好是2的整数倍,所以c的存储结果是 #12到#13,
- d的内存开始编号是 #14,存储1个字节,同理,d的存储结果就是#14
所以struct1 的内存实际分配为#0 到#14,15个,按照字节对齐,所以是打印结果是16;
struct2的结果分析
- a 的存储结果 是 #0 到 #7,存储8个字节,
- b的内存开始编号为#8 ,存储2个字节,,8 又正好是2 的整数倍 , 所以 b的存储结果是#8 到 #9,
- c 的内存开始编号是 #10,存储4个字节,10不是4的整数倍,所以c的存储编号移动到 #12,所以c的存储结果是 #12到#15;
- d的内存开始编号是 #16,存储1个字节,同理,d的存储结果就是#16
所以struct1 的内存实际分配为#0 到#16,17个,按照字节对齐,所以是打印结果是24;
大致的草图是这样的:仅个人理解,
图片.png
-
嵌套结构体内存对齐
再次,我又在项目的两个结构体中,加入了CGPoint 的一个成员,因为CGPoint本身就是一个结构体类型。
struct Student{
CGPoint p;
long a;
int b;
short c;
char d;
}struct1;
struct Tearcher{
long a;
short b;
int c;
char d;
CGPoint p;
}struct2;
再次打印相关的情况如下
图片.png
从上图打印我们发现,CGFloat 的内存分配是 8,CGPoint 的类存是16,也就是坐标(x,y)的内存字节的和,
原理在简单的结构体内存对齐已经介绍了,接下来说一下我发现的问题,
问题:为什么结构体嵌套,不是作为一个整体来存储?
从上图我们知道,如果按我们简单的结构体存储,那么struct1很好理解,但是struct 这时存储的位置是17了,按照整数倍的关系的话,内存编号应该移到32才开始存储CGPoint才对,照我想象的话最后的结果应该是47才对,然而结果打印的是40,这就是验证了一个问题,即使嵌套的结构体,系统也是把每个结构体的成员拆除出来单独存储,这样才能达到这样的结果。
嵌套struct1分析
- CGPoint 的 x 占据8个字节,存储是从#0到#7 ,存储8个字节;
- CGPoint 的 y的内存编号是 #8,存储8个字节,#8又正好是8的整数倍,所以y的存储结果是#8到#15;
- a 的存内存编号是 #16, 存储8个字节,#16正好有事8的整数倍,所以 a 的存储结果是 #16 到#23;
- b的内存开始编号为#24 ,存储4个字节,,#24 又正好是4 的整数倍 , 所以 b的存储结果是#24 到 #17,
- c 的内存开始编号是 #28,存储2个字节,#28又正好是2的整数倍,所以c的存储结果是 #28到#29,
- d的内存开始编号是 #30,存储1个字节,同理,d的存储结果就是#30
所以嵌套struct1 的内存实际分配为#0 到#30,31个,按照字节对齐,所以是打印结果是32;
嵌套struct2分析
- a 的存储结果 是 #0 到 #7,存储8个字节,
- b的内存开始编号为#8 ,存储2个字节,,8 又正好是2 的整数倍 , 所以 b的存储结果是#8 到 #9,
- c 的内存开始编号是 #10,存储4个字节,10不是4的整数倍,所以c的存储编号移动到 #12,所以c的存储结果是 #12到#15;
- d的内存开始编号是 #16,存储1个字节,同理,d的存储结果就是#16
- CGPoint 的 x 存储编号是#17 ,存储8个字节;#17不是8的整数倍,所以需要移动编号到#24;所以CGPoint 的 x 存储结果是#24到#31;
- CGPoint 的 y的内存编号是 #32,存储8个字节,#32又正好是8的整数倍,所以y的存储结果是#32到#39;
所以嵌套struct2 的内存实际分配为#0 到#39,40个,按照字节对齐,所以是打印结果是40;
总结
系统按照相关的内存对齐原则进行分配内存,嵌套类型不是作为一个整体来存储,而是吧嵌套子类型里边的类型逐个进行内存分配,这样既能节约内存消耗也能加快查询速度,通过自我学习,确实纠正了自己的一些错误观念。以后继续努力。
网友评论