iOS内存对齐

前言

相信大家都有过便利店和超市的购物经历,大家在购物特别是要买多种商品囤货的时候,大部分人都会选择去大超市吧。便利店的货架通常都是摆满为主,同一个货架上可能放着零食、日用品、饮料,到店里买东西一般都是直接叫老板,自己去找很麻烦。而大超市同一个货架通常是同一类商品,即使摆不满一个货架,那就摆到另一个货架上,同一个货架就算摆不满,但摆的也是同一类商品,毕竟大超市,地方大,顾客购物效率也会高一点。

通常字节是内存的基本单位,但是CPU在操作数据时,常常以为单位进行存取。如果没有一套规则去约束内存的存放,有些数据就会分布在不同的块区,这样CPU去找这些数据就会很累很烦(效率低)。字节对齐后,CPU找数据就不用跑来跑去(降低存取次数),相对的也就提高了效率。

对象内存的影响因素

接下来上代码分析一下,首先声明一个类:

@interface SLPerson : NSObject
@property(strong,nonatomic)NSString * name;
@property(assign,nonatomic)int age;
@end

研究对象类型的内存大小、对象实际的内存大小以及系统分配内存的大小:

    SLPerson * p = [SLPerson alloc];
    p.name = @"person";
    p.age = 18;
    
    SLPerson * p1;
    
    NSLog(@"p对象类型的内存大小--%lu",sizeof(p));
    NSLog(@"p对象实际的内存大小--%lu",class_getInstanceSize([p class]));
    NSLog(@"p系统分配的内存大小--%lu",malloc_size((__bridge const void *)(p)));
    NSLog(@"==================");
    NSLog(@"p1对象类型的内存大小--%lu",sizeof(p1));
    NSLog(@"p1对象实际的内存大小--%lu",class_getInstanceSize([p1  class]));
    NSLog(@"p1系统分配的内存大小--%lu",malloc_size((__bridge const void *)(p1)));
2022-07-01 10:38:26.985044+0800 alloc分析[29679:2671728] p对象类型的内存大小--8
2022-07-01 10:38:26.985110+0800 alloc分析[29679:2671728] p对象实际的内存大小--24
2022-07-01 10:38:26.985138+0800 alloc分析[29679:2671728] p系统分配的内存大小--32
2022-07-01 10:38:26.985164+0800 alloc分析[29679:2671728] ==================
2022-07-01 10:38:26.985186+0800 alloc分析[29679:2671728] p1对象类型的内存大小--8
2022-07-01 10:38:26.985207+0800 alloc分析[29679:2671728] p1对象实际的内存大小--0
2022-07-01 10:38:26.985231+0800 alloc分析[29679:2671728] p1系统分配的内存大小--0

分析

  • 对象类型的内存:p和p1本质就是个指针,所以sizeof(p)和sizeof(p1)都是8字节
  • 对象实际的内存:由类的成员变量大小决定,name(8)、age(4)以及isa指针(8),8+4+8=20,嗯???不对啊,明明输出了24啊
  • 系统分配的内存:32,这。。。。。。越来越大了呢?后面我们会探究malloc来进行分析
  • 至于p1并没有初始化,所以对象的内容所占用的大小均为0

下面我们通过LLDB指令去查看对象p属性在内存中的显示

 我们可以看到,p的内容有3个8字节构成,尽管age只占4字节,但是前面4字节也补0了,即对象的内容大小进行了8字节对齐。也就解释了为什么p的内容加起来是20字节,却占了24字节。

下面我们给类SLPerson添加一个实例方法和类方法

@interface SLPerson : NSObject
@property(strong,nonatomic)NSString * name;
@property(assign,nonatomic)int age;

-(void)test1;
+(void)test2;
@end

同样输出

2022-07-01 10:41:39.059577+0800 alloc分析[29700:2673212] p对象类型的内存大小--8
2022-07-01 10:41:39.059640+0800 alloc分析[29700:2673212] p对象实际的内存大小--24
2022-07-01 10:41:39.059668+0800 alloc分析[29700:2673212] p系统分配的内存大小--32
2022-07-01 10:41:39.059689+0800 alloc分析[29700:2673212] ==================
2022-07-01 10:41:39.059712+0800 alloc分析[29700:2673212] p1对象类型的内存大小--8
2022-07-01 10:41:39.059733+0800 alloc分析[29700:2673212] p1对象实际的内存大小--0
2022-07-01 10:41:39.059758+0800 alloc分析[29700:2673212] p1系统分配的内存大小--0

总结

  • 成员变量属性会影响类的实例对象的内存大小。
  • 添加方法,对类的实例对象内存大小没有任何影响,方法不存在对象内。
  • 在添加成员变量的过程中,由于成员变量的数据类型是不一致的,向最大数据类型的成员变量对齐。继承自NSObject对象的类,默认字节对齐方式是8字节

结构体内存对齐

对象的本质其实是结构体,内存对齐实际上可以看做是结构体的内存对齐,接下来探究下结构体内存对齐。

无嵌套

以上结果可知,两个结构体的成员变量是一摸一样的,只是声明的顺序不一样,输出的sizeof也不一样了。

分析 

下面我们用f(x,y)来模拟成员变量的存储情况,其中x表示成员变量的初始位置,y表示成员变量的大小

SLStruct1:

  • a:占8字节,a = f(0,8)
  • b:占4字节,(0+8)%4 = 0,b = f(8,4)
  • c:占2字节,(8+4)%2 = 0,c = f(12,2)
  • d:占1字节,(12+2)%1 = 0,d = f(14,1)

SLStruct2:

  • a:占8字节,a = f(0,8)
  • d:占1字节,(0+8)%1 = 0,b = f(8,1)
  • b:占4字节,(8+1)%4 = 1,需往前移3个位置即到12,12可以被4整除,c = f(12,4)
  • d:占2字节,(12+4)%2 = 0,d = f(16,2)

总结

SLStruct1加起来的内存大小是15字节,SLStruct2加起来的内存大小是18字节,两个结构体中成员变量最大内存是8字节,所以SLStruct1与SLStruct2的实际内存大小必须是8的整数倍,自动补齐,向上取整,所以SLStruct1实际大小是16字节,SLStruct2实际大小是24字节。

有嵌套

接下来我们再创建一个结构体,里边嵌套一个结构体

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


打印结果
2022-07-01 11:24:29.010493+0800 哦哦、[2895:94727] SLStruct1-----16
SLStruct2-----24
SLStruct3-----40
Program ended with exit code: 0

分析 

同样地,我们来分析SLStruct3

SLStruct1:

  • a:占8字节,a = f(0,8)
  • b:占4字节,(0+8)%4 = 0,b = f(8,4)
  • c:占2字节,(8+4)%2 = 0,c = f(12,2)
  • d:占1字节,(12+2)%1 = 0,d = f(14,1)
  • lwStr:SLStruct2中成员最大的占8字节,所以(14+1)%8 = 7,需往前移1个位置即到16,16可以被8整除,c = f(16,18)

总结

SLStruct3加起来的内存大小是34字节,结构体中成员变量a和lwStr都占了最大内存是8字节,所以SLStruct3的实际内存大小必须是8的整数倍,自动补齐,向上取整,所以SLStruct3实际大小是40字节。

结构体对齐规则

通过以上实例分析,总结一下结构体是怎么计算大小的

  1. struct与union的成员,按顺序依次存放,第一个成员初始位置放在x=0的地方,后续成员存放的初始位置需存放在该成员大小的整数倍位置上。
  2. 假设成员中嵌套有结构体成员s,则s的初始位置要根据s中的最大成员大小来定,比如s中最大的成员占8字节,则s的初始位置需存放在8的整数倍位置上。
  3. 计算完最后一个成员的位置并加上该成员的大小后(假设为r),要看r是不是结构体中最大成员的整数倍,不足的则要补齐。
  4. 所以说类的对象计算其内容大小也是按照这个套路,无非就是类的对象比结构体一开始要多算一个isa(占8字节),然后其他成员(属性)往后排放

malloc探究

malloc_size方法是不直接提供的,这里我下载了libmalloc-317.40.8库去探究,之前在探究alloc时,在经过_class_createInstanceFromZone方法后,calloc即系统开辟内存,所以我们从calloc开始入手。

1.调用calloc方法

2.跳到calloc

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

3.跳到_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;
}

4.跳到zone->calloc,发现点不进去,借助汇编。

5.全局搜索 default_zone_calloc

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

6.又是zone->calloc,继续汇编

7.全局搜索nano_calloc

static void *
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);
}

8.进入_nano_malloc_check_clear,此时我们把焦点放在size_t上,发现了个很好的方法segregated_size_to_fit

static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
	MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 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]);

	ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
	if (ptr) {

。。。。。。。

9进入segregated_size_to_fit,此时此刻,我们终于在libmalloc源码找到了核心算法

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

同时我们查看到两个宏定义

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

可以得出,当我们调用calloc(1, 20)时,最后通过算法是通过(20 + 16 - 1) >> 4 << 4 操作 ,结果就是48,即内存对齐按照16字节对齐。(PS:右移一位相当于除以2,左移一位相当于乘以2)

总结

  1. class_getInstanceSize:获取实例对象所占内存大小(8字节对齐)
  2. malloc_size:获取系统实际分配的内存大小(16字节对齐)

猜你喜欢

转载自blog.csdn.net/weixin_38016552/article/details/125542716
今日推荐