iOS2 内存对齐

542 阅读7分钟

一,结构体题对齐原则

  1. 结构体每个成员相对于起始地址的偏移能够被其自身大小整除,如果不能则在前一个成员后面补充字节
  2. 结构体变量的起始地址能够被其最宽的成员大小整除,如果不能则在前一个成员后面补充字节
  3. 结构体总体大小能够被最宽的成员的大小整除,如不能则在后面补充字节

验证一下对齐原则

struct structA {
    double a;//[0-7]
    char b;//[8]
    int c;//原则1[9-11]要补齐,所以12开始[12,13,14,15]
    //原则3 成员最大8字节 (a,b,c最宽a为8)16刚好是8倍数 sizeofA-----16
};
struct structB {
    char a;//[0]
    struct structA b;//原则2 [1-7]要补齐,所以8开始[8-23]
    int c;//[24-27] 原则3,(a,b,c最宽b为8)[28-31]要补齐 sizeofB-----32
};

输出验证

struct structA a;
NSLog(@"sizeofA-----%zu",sizeof(a));
struct structB b;
NSLog(@"sizeofB-----%zu",sizeof(b));

输出日志

2021-07-20 14:56:51.861184+0800 xzwblue[6728:183043] sizeofA-----16
2021-07-20 14:56:51.861613+0800 xzwblue[6728:183043] sizeofB-----32

对于结构体的计算只需要完全理解对齐三原则基本ok

基本数据类型内存

COC32位64位
boolBOOL(64位)11
signed char(__signed char)int8_t、BOOL(32位)11
unsigned charBoolean11
shortint16_t22
unsigned shortunichar22
int 、int32_tNSInteger(32位)、boolean_t(32位)44
unsigned intboolean_t(64位)、NSUInteger(32位)44
longNSInteger(64位)48
unsigned longNSUInteger(64位)48
long longint64_t88
floatCGFloat(32位)44
doubleCGFloat(64位)88

二,开辟内存

  • 新建对象
@interface NBPerson : NSObject
@property (nonatomic, assign) char flag;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
//@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, assign) float height;

@end
  • 输出测试
#import <objc/runtime.h>
#import <malloc/malloc.h>


NBPerson *nb = [[NBPerson alloc] init];
NSLog(@"对象类型的内存大小--%lu",sizeof(nb));
NSLog(@"对象占用内存大小--%lu",class_getInstanceSize([nb class]));
NSLog(@"系统分配的内存大小--%lu",malloc_size((__bridge const void *)(nb)));

//输出结果
2021-07-20 16:32:36.045471+0800 xzwblue[8458:248752] 对象类型的内存大小--8
2021-07-20 16:32:36.045907+0800 xzwblue[8458:248752] 对象占用内存大小--40
2021-07-20 16:32:36.046018+0800 xzwblue[8458:248752] 系统分配的内存大小--48
  1. sizeof 是一个运算符,并不是一个函数。 sizeof 传进来的是类型,用来计算这个类型占多大内存,这个在 编译器编译阶段 就会确定大小并直接转化成 8 、16 、24 这样的常数,而不是在运行时计算。参数可以是数组、指针、类型、对象、结构体、函数等

它的功能是:获得保证能容纳实现所建立的最大对象的字节大小。

由于在编译时确定占用内存大小,因此sizeof不能用来返回动态分配的内存空间的大小。

nb被当作一个指针,指针类型内存大小为8

  1. class_getInstanceSize 用于获取类的实例对象所占用的内存大小,并返回具体的字节数,其本质就是获取实例对象中成员变量的内存大小

依赖于<objc/runtime.h>,返回创建一个实例对象所需内存大小

查看源码

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

uint32_t unalignedInstanceSize() const {
    ASSERT(isRealized());
    return data()->ro()->instanceSize;
}

// 返回一个类的ivar(成员变量)所占空间的大小
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

#  define WORD_MASK 7UL   

// 计算占用内存大小算法 (8字节对齐)
static inline uint32_t word_align(uint32_t x) {
  //           (8 + 7) & ~ 7
    return (x + WORD_MASK) & ~WORD_MASK;
}

  1. malloc_size 查看源码 计算对象实际分配的内存大小,这个是由系统完成的(采用16字节对齐),

依赖于<malloc/malloc.h>,返回系统实际分配的内存大小

malloc源码分析中的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 + 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;
}

  1. 内存优化
  • class_getInstanceSize和结构体内存计算是一样的都会遵守内存对齐原则
  • 更骚的是还有优化功能,也就是结构体的内存计算会被成员顺序影响,对象的内存计算,不管你属性是怎么样排列的,系统都会按照最优的顺序,将数据存储到内存中去。
  1. 自己计算 上面这些都弄懂之后我们自己就可以计算内存了
@interface NBPerson : NSObject

@property (nonatomic, assign) char flag; /1
@property (nonatomic, copy) NSString *name;/8
@property (nonatomic, copy) NSString *nickName;/8
//@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *sex;/8
@property (nonatomic, assign) float height;/4

最优顺序计算
8(isa)+3*8+(4+1)8 = 40
class_getInstanceSize = 40
16字节对齐 malloc_size = 48

三,验证优化

@property (nonatomic, assign) char flag;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
//@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, assign) float height;


    NBPerson *nb = [[NBPerson alloc] init];
    nb.flag = 'a';
    nb.name = @"牛逼大佬";
    nb.nickName = @"真牛逼";
    nb.sex = @"男";
    nb.height = 180.3;
(lldb) x/6gx nb
0x600001940ff0: 0x000000010398cca0 0x43344ccd00000061
0x600001941000: 0x00000001039859d8 0x00000001039859f8
0x600001941010: 0x0000000103985a18 0x0000000000000000

//很直观的
0x000000010398cca0 --- isa
0x0000000000000000 --- 内存补齐


//这个不是8字节数据
(lldb) po 0x43344ccd00000061
4842579942682132577

//类型不对
(lldb) po 0x43344ccd
1127501005

(lldb) po/f 0x43344ccd
180.300003

(lldb) po/c 0x00000061
a\0\0\0

(lldb) po 0x00000001039859d8
牛逼大佬

(lldb) po 0x00000001039859f8
真牛逼

(lldb) po 0x0000000103985a18

一顿操作之后发现这个char和float虽然在顺序上不在一起,但是系统会自动最优化,把他们放到一起,不得不说,的确有过人之处

四,问题探索

  1. 结构体为什么要字节对齐?
字节对齐的作用不仅是便于cpu快速访问,同时合理的利用字节对齐可以有效地节省存储空间
  1. 什么是实例对齐?
简单的说就是在开辟一块内存空间的时候,只能开辟16的整数倍字节大小的内存空间。这样以16位为单位长度开辟空间,就可以保证内存的连续性上的整齐度。
  1. 为什么要16字节对齐?
大家都知道字节是内存的容量单位,1内存你也可以叫它1字节。但是,CPU在读取内存的时候,却不是以字节为单位来读取的,而是以“块”为单位读取的,所以大家也经常听到一块内存,“块”的大小也就是内存存取的力度。

如果不对齐的话,在我们频繁的存取内存的时候,CPU就需要花费大量的精力去分辨你要读取多少字节,这就会造成CPU的效率低下,如果想要CPU又有效又不减少存取次数的话,那就需要找一个规范,这个规范就是字节对齐。

苹果采取16字节对齐,是因为OC的对象中,第一位叫`isa`指针,它是必然存在的,而且它就占了8位字节,就算你的对象中没有其他的属性了,也一定有一个`isa`,那对象就至少要占用8位字节。如果以8位字节对齐的话,如果连续的两块内存都是没有属性的对象,那么它们的内存空间就会完全的挨在一起,是容易混乱的。

以16字节为一块,这就保证了CPU在读取的时候,按照块读取就可以,效率更高,同时还不容易混乱。