IOS 底层之内存对齐

1,480 阅读10分钟

前言

内存对齐这个词总是环绕在我们耳边,但是真正的含义我们却一知半解,总感觉被迷雾笼罩,今天就探索下内存对齐,把它神秘的面纱揭开。俗话说实践是检验真理的唯一标准,直接上案例。下面打印对象类型的内存大小,对象实际的内存大小和系统分配的内存大小。代码和打印如下:

LWPerson * person =  [LWPerson alloc];
person.name = @"person";
person.age = 18;
LWPerson * newPerson;

NSLog(@"对象类型的内存大小--%lu",sizeof(person));
NSLog(@"对象实际的内存大小--%lu",class_getInstanceSize([person class]));
NSLog(@"系统分配的内存大小--%lu",malloc_size((__bridge const void *)(person)));
NSLog(@"==================");
NSLog(@"对象类型的内存大小--%lu",sizeof(newPerson));
NSLog(@"对象实际的内存大小--%lu",class_getInstanceSize([newPerson  class]));
NSLog(@"系统分配的内存大小--%lu",malloc_size((__bridge const void *)(newPerson)));
2021-06-08 11:16:28.097465+0800 alignStyle[73542:9731629] 对象类型的内存大小--8
2021-06-08 11:16:28.097520+0800 alignStyle[73542:9731629] 对象实际的内存大小--24
2021-06-08 11:16:28.097562+0800 alignStyle[73542:9731629] 系统分配的内存大小--32
2021-06-08 11:16:28.097583+0800 alignStyle[73542:9731629] ==================
2021-06-08 11:16:28.097607+0800 alignStyle[73542:9731629] 对象类型的内存大小--8
2021-06-08 11:16:28.097629+0800 alignStyle[73542:9731629] 对象实际的内存大小--0
2021-06-08 11:16:28.097649+0800 alignStyle[73542:9731629] 系统分配的内存大小--0

结果分析:

  • sizeof:对象类型的内存大小,sizeof 是一个操作符号,不是函数。计算的是传进来的数据类型的大小,这个在编译时期就已经确定。所以 sizeof(person)sizeof(newPerson) 都是8字节,因为它们的本质是结构体指针
  • class_getInstanceSize:对象实际的内存大小,内存大小是由类的成员变量的大小决定。实际上并不是严格意义上的对象的内存的大小,因为内存进行了8字节对齐,核心算法是define WORD_MASK 7UL ((x + WORD_MASK) & ~WORD_MASK。 所以 person 的内存大小是 24 而不是 20newPerson 只是声明了一个变量,并没有开辟内存,所以大小是0
  • malloc_size 系统分配的内存大小是按16字节对齐的方式,即分配的大小是16的倍数 ,不足16的倍数系统会自动填充字节,注意系统的16字节对齐是在实际的内存大小(经过8字节对齐)的基础上。

问题:
class_getInstanceSizemalloc_size 底层做了什么? 你怎么知道
class_getInstanceSize8字节对齐的呢? 而malloc_size16字节对齐的呢? class_getInstanceSizemalloc_size探索流程会放在文章的结尾。

内存对齐

前言我们发现对象实际的内存大小是8字节对齐,那么到底是怎么对齐的。引出我们的重点内存对齐。

各类型所占字节

在这里整理些基本数据类型在不同系统下的字节大小,方便大家查看。

基本数据类型字节表.jpg

需要内存对齐的原因

  • 内存是以字节为基本单位,cpu在存取数据时,是以为单位存取,并不是以字节为单位存取。频繁存取未对齐的数据,会极大降低cpu的性能。字节对齐后,会减低cpu的存取次数,这种以空间换时间的做法目的降低cpu的开销。
  • cpu存取是以块为单位,存取未对齐的数据可能开始在上一个内存块,结束在另一个内存块。这样中间可能要经过复杂运算在合并在一起,降低了效率。字节对齐后,提高了cpu的访问速率。

内存对齐原则

  1. 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在 32 位机为4字节,则要从4的整数倍地址开始存储。

  2. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)

  3. 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍.不足的要补齐。

内存对齐原则描述的那么复杂,搞我心态是吧,必须实例解我疑惑

结构体内存对齐(无嵌套)

对象的本质就是结构体,对象的底层实现是结构体。内存对齐实际上可以看做是结构体内存对齐,只不过系统对实例化的对象进行了内存优化。接下来实例探究下结构体内存对齐

struct LWStruct1{
    double  a; // 8
    int     b; // 4
    short   c; // 2
    char    d; // 1
}LWStruct1;


struct LWStruct2{
    double  a; // 8
    char    d; // 1
    int     b; // 4
    short   c; // 2

}LWStruct2;
int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"-----%lu-----%lu",sizeof(LWStruct1),sizeof(LWStruct2));
    }
    return 0;
}
2021-06-08 15:02:54.392903+0800 alignStyle[74021:9798038] -----16-----24

结果分析发现 LWStruct1LWStruct2 所包含的变量是一样的,只是位置不一样,但是内存大小不一样,为什么? 这就是结构体内存对齐。

下面就根据内存对齐原则进行简单的计算和分析
LWStruct1内存大小详细过程(min(m,n) m表示当前开始的位置,n表示大小)

  • 变量a: 占8个字节,offert0开始, min(0,8), 即0 ~ 7 存放a
  • 变量b: 占4个字节,offert8开始, min(8,4), 即8 ~ 11 存放b
  • 变量c: 占2个字节,offert12开始,min(12,2),即12 ~ 13 存放c
  • 变量d: 占1个字节,offert14开始,min(14,1),即14 存放d 结果显示 LWStruct1 的实际的内存大小是15字节,LWStruct1中最大的变量是a占个 8 字节。所以LWStruct1的实际内存大小必须是8的整数倍,15不是8的整数倍,向上取整,不足的自动补齐为16字节。最后LWStruct1的内存大小为16字节。

LWStruct1解析图如下

Pasted Graphic.png

LWStruct2内存大小详细过程

  • 变量a: 占8个字节,offert0开始, min(0,8), 即0 ~ 7 存放a
  • 变量d: 占1个字节,offert8开始, min(8,1), 即8 存放d
  • 变量b: 占4个字节,offert9开始, min(9,4)9 % 4 != 0,继续往后移动直到找到可以整除4的位置 1212 ~ 15 存放b
  • 变量c: 占2个字节,offert16开始,min(16,2),即16 ~ 17 存放c 结果显示 LWStruct2 的实际的内存大小是18字节,LWStruct2中最大的变量是a占个 8 字节。所以LWStruct2的实际内存大小必须是8的整数倍,18不是8的整数倍,向上取整,不足的自动补齐为24字节。最后LWStruct2的内存大小为24字节。

LWStruct2解析图如下

Frontonioouniiiooiiooousiio-ooh    -myeowvcuh.png

结构体中嵌套结构体

 struct LWStruct1{
    double  a; // 8
    int     b; // 4
    short   c; // 2
    char    d; // 1
}LWStruct1;


struct LWStruct2{
    double  a; // 8
    char    d; // 1
    int     b; // 4
    short   c; // 2
 
}LWStruct2;

struct LWStruct3{
    long    a; // 8
    int     b; // 4
    short   c; // 2
    char    d; // 1
    struct LWStruct2 lwStr;
}LWStruct3;

int main(int argc, char * argv[]) {
    @autoreleasepool {
      NSLog(@"-----%lu-----%lu----%lu",sizeof(LWStruct1),sizeof(LWStruct2),sizeof(LWStruct3));
    }
    return 0;
}
 
2021-06-08 16:28:40.819854+0800 alignStyle[74082:9819949] -----16-----24----40

LWStruct3内存大小详细过程

  • 变量a: 占8个字节,offert0开始, min(0,8), 即0 ~ 7 存放a
  • 变量b: 占4个字节,offert8开始, min(8,4), 即8 ~ 11 存放b
  • 变量c: 占2个字节,offert12开始,min(12,2),即12 ~ 13 存放c
  • 变量d: 占1个字节,offert14开始,min(14,1),即14 存放d
  • 变量lwStr:lwStr是结构体变量,内存对齐原则结构体成员要从其内部最大元素大小的整数倍地址开始存储LWStruct2 中的最大的变量占8字节,所以offert16开始,LWStruct2的内存大小是18字节。min(16,18),即18 ~ 33存放 lwStr

结果显示 LWStruct3 的实际的内存大小是34字节,LWStruct3中最大的变量是lwStra都是 8 字节。所以LWStruct3的实际内存大小必须是8的整数倍,34不是8的整数倍,向上取整,不足的自动补齐为40字节。最后LWStruct3的内存大小为40字节。

LWStruct3解析图如下

image.png

内存优化

结构体内存根据变量位置的顺序最后内存的大小可能不一样,实例化对象的内存对齐会不会也会出现这种情况呢。那就探究下呗

    LWPerson * person =  [LWPerson alloc];
    person.a = 100.0;
    person.b = 'a';
    person.c = 10;
    person.d = 2;
    
    NSLog(@"----%lu",class_getInstanceSize([person class]));
2021-06-08 17:11:27.743037+0800 alignStyle[74104:9830701] ----24

LWPerson 中自定义的变量和 LWStruct2 的变量的顺序是一模模一样样,但是对象自带一个变量isa指针占8字节。所以LWPerson 中自定义的变量占了16个字节,奇怪了啊,结构体的顺序,名字都是一样的啊。这是为什么呢?这就是下面要说的内存优化(系统多机智啊浪费那么多内存,当然得优化啊),具体看看是怎么优化的

image.png

通过lldb断点打印可以看出 ,a的读取通过 0x4059000000000000,b的读取通过0x61(a的ASCII码是97)c的读取通过 0x0ad的读取通过0x02, 神奇的发现 char b,int c,short d,共用了一个8字节内存空间,对象的属性或者变量存储顺序和结构体的也不一样进行了重排。系统进行了内存优化。

总结

内存对齐有制定了一套规则,目的是提高cpu的存取效率和安全的访问。字节对齐可能浪费了部分内存,但是同时进行内存优化尽可能的降低了内存的浪费,即保证了存取的速率,又减少了内存的浪费,不得不说真的很优秀啊。

补充

class_getInstanceSize 探究

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

进入 alignedInstanceSize

 uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }

进入 word_align

#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

总结:word_align多么熟悉的词,就不多说了好吧。

malloc_size 探究

malloc_size 想进去看看里面具体是怎么实现的,但是点击去如下图

image.png

malloc_size方法实现没有提供,怎么办呢?看文件路径可能在malloc库,下载libSystem_malloc库编译运行,malloc_size获取系统分配的内存大小 calloc是系统开辟内存,当然大小肯定也会计算分配,所以我们探究calloc方法

void *p = calloc(1, 40);

进入 calloc

calloc(size_t num_items, size_t size)
{
	return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}

进入 _malloc_zone_calloc

_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
		malloc_zone_options_t mzo)
{
	MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

	void *ptr;
	if (malloc_check_start) {
		internal_check();
	}
        //这是关键,别问,问就是一下就看到了
	ptr = zone->calloc(zone, num_items, size);

	if (os_unlikely(malloc_logger)) {
		malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
				(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
	}

	MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
	if (os_unlikely(ptr == NULL)) {
		malloc_set_errno_fast(mzo, ENOMEM);
	}
	return ptr;
}

进入 zone->calloc此时发现点不进去,怎么办呢?我用的是汇编方法,直接看汇编

BB   1 m main.m.png

dylib^default_zone_calloc.png

全局搜索 default_zone_calloc

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);
}

进入 zone->calloc此时发现又点不进去,老办法看汇编

dylib^default_zone_calloc.png

全局搜索 nano_calloc

nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
	size_t total_bytes;

	if (calloc_get_size(num_items, size, 0, &total_bytes)) {
		return NULL;
	}

	if (total_bytes <= NANO_MAX_SIZE) {
		void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
		if (p) {
			return p;
		} else {
			/* FALLTHROUGH to helper zone */
		}
	}
	malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
	return zone->calloc(zone, 1, total_bytes);
}

进入 _nano_malloc_check_clear 因为我们现在只关心内存的大小所以直接看size_t

image.png

进入 segregated_size_to_fit

#define SHIFT_NANO_QUANTUM		4
#define NANO_REGIME_QUANTA_SIZE	(1 << SHIFT_NANO_QUANTUM)	// 16

static MALLOC_INLINE size_t
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 + 16 - 1) >> 4 左移4位
	k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
        // round up and shift for number of quanta
        // slot_bytes = k << 4   右移4位
	slot_bytes = k << SHIFT_NANO_QUANTUM;	
        // multiply by power of two quanta size
	*pKey = k - 1;					
        // Zero-based!
	return slot_bytes;
}

总结k >> 4 k << 4 16进制对齐 流程图后面补吧