OpenGL & Metal

iOS底层原理--内存对齐

2020-09-09  本文已影响0人  黑眼豆豆_

iOS底层原理--alloc&init&new这篇文章中,我们认识到了字节对齐
那么,我们回顾一下什么是字节对齐。

字节对齐

假如一个创建一个对象LGPerson

    //创建LGPerson
     LGPerson *p1 = [LGPerson alloc];

通过调用

//获取size
size = cls->instanceSize(extraBytes);

之后得到size = 16

接下来我们调用class_getInstanceSize(这个方法是获取类对象的实际内存)

// size  = 8
size_t size = class_getInstanceSize([LGPerson class])

最终得到的size = 8

这就验证了字节对齐的存在,而且由于实际大小为8,所以字节对齐的最小值是16个字节。

内存对齐

很多 CPU拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。而且读取未对齐的数据,会大大降低 CPU 的性能。
CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。每次内存存取都会产生一个固定的开销,减少内存存取次数将提升程序的性能。所以 CPU 一般会以 2/4/8/16/32 字节为单位来进行存取操作。我们将上述这些存取单位也就是块大小称为(memory access granularity)内存存取粒度。

我们用一段代码来解释内存对齐

struct LGStruct1 {
    double a;   //占8位
    char b;     //占1位
    int c;      //占4位
    short d;    //占2位
}struct1;

struct LGStruct2 {
    double a;    //占8位
    int b;       //占4位
    char c;      //占1位
    short d;     //占2位
}struct2;

接下来用sizeof(strut)打印结构体的内存大小,照理说2个结构体的内容一样,只是排布顺序不同,大小应该一致。那么打印结果如下:

打印结构体.png
明显看到,2个结构体的内存大小不一样,这就是内存对齐产生的影响。

内存对齐的原则

内存对齐的解释

上面的话太官方了,我们用比较通俗的语言来解释。

针对上面struct1这个案例来分析,首先我们已经标注出每个成员所占的内存大小。

struct LGStruct1 {
    double a;   //占8位
    char b;     //占1位
    int c;      //占4位
    short d;    //占2位
}struct1;

具体类型在c/oc中所占内存大小如图:


各数据类型所占内存.png

所以按照规则来进行操作:

如上图,a从0开始,长度为8,所以截止到7

所以最终长度为24。

接下来,我们用上面的规则来看下struct2这个例子。

struct LGStruct2 {
    double a;    //占8位
    int b;       //占4位
    char c;      //占1位
    short d;     //占2位
}struct2;

还是通过示意图来来解读。


struct2

最终得出了struct2的长度为16。

结构体的嵌套

接下来,我们来下面这个例子,结构体struct2中嵌套了一个结构体struct1

struct Struct1 {
    double a;  //占8位
    char b;    //占1位
    int c;     //占4位
    short d;   //占2位
}struct1;

struct Struct2 {
    double e;  //占8位
    int f;     //占4位
    char g;    //占1位
    short h;   //占2位
    struct Struct1 I;
}struct2;

所以,再看内存对齐的规则会更加清晰。

属性重排(内存优化)

我们知道,所有的oc对象本质上是一个结构体,那么如果按照内存对齐对齐的原则的话,我们一定要特别注意属性存放的位置。因为结构体的大小,和结构体成员的排列顺序有关。
但是实际上,并没有人去关注对象属性的位置。这就是属性重排的作用。
通过一个例子来看。

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
 @property (nonatomic, copy) NSString *hobby;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;

@property (nonatomic) char c1;
@property (nonatomic) char c2;
@end
NS_ASSUME_NONNULL_END
//赋值
LGPerson *person = [LGPerson alloc];
person.name      = @"Cooci";
person.nickName  = @"KC";
person.age       = 18;
person.c1        = 'a';
person.c2        = 'b';

通过ASCII换算得出97为a,98为b。

我们用一张图来表示


LGPerson.png

对象的内存对齐

我们用一段代码来展示,还是之前的对象,我们来打印一下LGPerson

LGPerson *person = [LGPerson alloc];
person.name      = @"Cooci";
person.nickName  = @"KC";
NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([LGPerson class]),malloc_size((__bridge const void *)(person)));

打印结果如下:


LGPerson.png

对打印信息进行分析

//定义WORD_MASK为7UL
#   define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

跟之前的分析方式一样,传进去的x = 24,WORD_MASK = 7,相加后

// 7 + 31
x + WORD_MASK = 31

转换成二进制

//31的二进制
0000 0000 0001 1111

~WORD_MASK表示WORD_MASK的二进制取反

//7的二进制取反
1111 1111 1111 1000

最后经过与运算,得到

   //31的二进制
   0000 0000 0001 1111
   //7的二进制取反
&1111 1111 1111 1000
   //最终结果
= 0000 0000 0001 1000

可以看到,前面3位都抹0,所以可以得出结论,对象创建的时候,真正进行的是8位的字节对齐

目前已知的16字节内存对齐算法有两种:
alloc源码分析中的align16
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
0000 0000 0000 0001
//左移4位
0000 0000 0001 0000   = 16
//55的二进制
0000 0000 0011 0111
//左移4位
0000 0000 0000 0011
//k的二进制
0000 0000 0000 0011
//右移4位
0000 0000 0011 0000

可以看到,前面4位都抹0,所以可以得出结论,系统进行内存分配的时候,进行了16位的内存对齐。

结论

虽然我们的结构体遵循内存对齐的原则,但是,它不会随意的浪费内存,会通过内存重排的方式进行结构优化,将多余的内存进行合理的利用,尽最大的可能节省内存。

上一篇 下一篇

猜你喜欢

热点阅读