2. 内存对齐&isa

421 阅读10分钟

内存对⻬

不同类型的数据在内存中按照一定的规则排列;而不是顺序的一个接一个的排放,这就是对齐。

为什么需要内存对齐?

  • CPU对内存的读取不是连续的,而是分块读取的,块的大小只能是1、2、4、8、16字节
  • 当读取操作的数据未对齐,则需要两次总线周期来访问内存,因此性能会大打折扣
  • 某些硬件平台只能从规定的地址处取某些特定类型的数据,否则则抛出硬件异常    

内存对⻬的原则

  1. 数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址(如4,8,12...)开始存储。
  2. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储.)
  3. 收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍,不足的要补⻬。

比如

struct MyStruct{
    char a; //1 [0] (1,2,3) 
    int  b; //4 [4,5,6,7] 自对齐,从int大小的整数倍开始存,(1,2,3)空着
    char c; //1 [8] (9,10,11)  0-8共9字节,总大小是内部最大成员的整数倍即4的整数倍
}MyStruct;
sizeof(MyStruct)=12
struct MyStruct1{
    char a;//1 [0]
    char c;//1 [1](2,3) 
    int  b;//4 [4,5,6,7]  //自对齐,(2,3)空着,正好8字节
}MyStruct1;
sizeof(MyStruct1)=8;

可见变量定义的位置不同,有可能影响结构体的大小

指定#pragma pack的情况 它能够改变编译器的默认对齐方式 里面的值只能是 '1', '2', '4', '8', or '16'

  • 第一个成员起始于0偏移处
  • 每个成员按其类型大小和指定对齐参数n中较小的一个进行对齐
    • 偏移地址和成员占用大小均需对齐
    • 结构体成员的对齐参数为其所有成员使用的对其参数的最大值    
  • 结构体的总长度必须为所有对其参数的整数倍    
  • #pragma pack()表示取消指定对齐,恢复缺省对齐
#pragma pack(1)
struct A{
    char a;//1 [0] 
    int  b;//4 [1,2,3,4]
    char c;//1 [5]   占6字节
}A;
sizeof(A)=6;

#pragma pack(2)
struct B{
    char a;//1 [0](1) pack(2)
    int  b;//4 [2,3,4,5]
    char c;//1 [6](7)  占8字节
}B;
sizeof(B)=8;
`pack(4),pack(8),pack(16)`时都是12
#pragma pack(1)
struct A{
    char a;//1 [0] 
    double  b;//8 [1,2,3,4,5,6,7,8]
    char c;//1 [9]   占10字节
}A;
sizeof(A)=10;

#pragma pack(2)
struct B{
    char a;//1 [0](1)  
    double  b;//8 [2,3,4,5,6,7,8,9]
    char c;//1 [10](11)  占12字节
}B;
sizeof(B)=12;

#pragma pack(4)
struct C{
    char a;//1 [0](1,2,3)  
    double  b;//8 [4,5,6,7,8,9,10,11]
    char c;//1 [12](13,14,15)  占16字节
}C;
sizeof(C)=16;

`pack(8),pack(16)`时都是24

指定__attribute__((aligned(n)))的情况 里面的值是 2的指数,如1,2,4,8,16...

  • 该声明将强制编译器确保(尽可能)变量类型为structS或者int32_t的变量在分配空间时采用n字节对齐方式。
  • 如果aligned后面不指定数值__attribute__((aligned));,那么编译器将依据你的目标机器情况使用最大最有益的对齐方式。
#include <stdio.h>

struct S1 {
    short b[3];
};

struct S2 {
    short b[3];
} __attribute__((aligned(8)));

struct S3 {
    short b[3];
} __attribute__((aligned(16)));

struct S4 {
    short b[3];
} __attribute__((aligned(32)));

struct S5 {
    short b[3];
} __attribute__((aligned(64)));

struct S6 {
    short b[3];
} __attribute__((aligned));

int main(int argc, char** argv)
{
    printf("sizeof(struct S1) = %ld\n", sizeof(struct S1));
    printf("sizeof(struct S2) = %ld\n", sizeof(struct S2));
    printf("sizeof(struct S3) = %ld\n", sizeof(struct S3));
    printf("sizeof(struct S4) = %ld\n", sizeof(struct S4));
    printf("sizeof(struct S5) = %ld\n", sizeof(struct S5));
    printf("sizeof(struct S6) = %ld\n", sizeof(struct S6));

    return 0;
}
/*
 * 输出结果:
 * sizeof(struct S1) = 6
 * sizeof(struct S2) = 8
 * sizeof(struct S3) = 16
 * sizeof(struct S4) = 32
 * sizeof(struct S5) = 64
 * sizeof(struct S6) = 16
*/

__attribute__((__packed__))的情况

使用该属性对struct或union类型进行定义,设定其类型的每一个变量的内存约束。当用在enum类型定义时,暗示了应该使用最小完整的类型(it indicates that the smallest integral type should be used)。

#include <stdio.h>

struct unpacked_struct {
    char c;
    int i;
};

struct packed_struct_1 {
    char c;
    int i;
} __attribute__((__packed__));

struct packed_struct_2 {
    char c;
    int i;
    struct unpacked_struct us;
} __attribute__((__packed__));

int main(int argc, char** argv)
{
    printf("sizeof(struct unpacked_struct) = %lu\n", sizeof(struct unpacked_struct));
    printf("sizeof(struct packed_struct_1) = %lu\n", sizeof(struct packed_struct_1));
    printf("sizeof(struct packed_struct_2) = %lu\n", sizeof(struct packed_struct_2));

    return 0;
}

/*
 * 输出:
 * sizeof(struct unpacked_struct) = 8
 * sizeof(struct packed_struct_1) = 5
 * sizeof(struct packed_struct_2) = 13
*/

aligned属性被设置的对象占用更多的空间,相反的,使用packed可以减小对象占用的空间。

参考gcc之__attribute__简介及对齐参数介绍

复杂点的情况

struct LGStruct1 {
    char   a;   // 1 [0]
    double b;   // 8 (1-7) [8-15]
    int    c;   // 4 [16-19]
    short  d;   // 2 [20-21]  占了22字节,最大为8,所以是8的整数倍, 大于22的8的最小倍数为24;
} MyStruct1;

struct LGStruct2 {
    double b;   // 8  [0-7]
    char   a;   // 1  [8]
    int    c;   // 4  (9-11) [12-15]
    short  d;   // 2  [16,17] 占了18字节,最大为8,所以是8的整数倍, 也是24;
} MyStruct2;

struct LGStruct3 {
    double b;   // 8 [0-7]
    int    c;   // 4 [8-11]
    char   a;   // 1 [12]
    short  d;   // 2 (13) [14-15] 占了16字节,最大为8,所以是8的整数倍正好16;
} MyStruct3;

struct LGStruct4 {
    double b;                 //8  [0-7]
    struct LGStruct1 struct1; //24 [8-31]  LGStruct1中成员最大长度为8,要从8的整数倍开始存
    char   a;                 //1  [32]
    short  d;                 //2  [34-35] 占了36字节,最大为8,大于36的最小8的整数倍40;
}MyStruct4;

在类中内存对齐又是怎样表现的呢?

# define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
    // (x + 7) >> 3 << 3
    return (x + WORD_MASK) & ~WORD_MASK;
}

根据源码得知,类的属性是8字节对齐

@interface LGTeacher : NSObject
@property (nonatomic, copy)   NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic, strong) NSString *hobby;
@property (nonatomic) char ch1;
@property (nonatomic) char ch2;
@end


LGTeacher  *p = [LGTeacher alloc];
p.name = @"DH";   // 8
p.age  = 18;      // 4
p.height = 185;   // 8
p.hobby  = @"女"; // 8
p.ch1 = 'a';
p.ch2 = 'b';

(lldb) x/6gx p
0x100ff7de0: 0x001d800100002545 0x0000001200006261
0x100ff7df0: 0x0000000100002080 0x00000000000000b9
0x100ff7e00: 0x00000001000020a0 0x0000000000000000
(lldb) po 0x00000012
18
(lldb) po 0x62
98
(lldb) po 0x61
97

二进制重排

age ch1 ch2排在了一起,如果按照对象默认的属性顺序进行内存分配,在进行属性的8字节对齐 时会浪费大量的内存空间,所以这里系统会帮我们把对象的属性重新排列来最大化利用我们的内存空间,这种操作被称为二进制重排。

iOS对象申请内存VS系统开辟的内存

LGTeacher  *p = [LGTeacher alloc];
                   // isa ---- 8
p.name = @"DH";    // 8
p.age  = 18;       // 4
p.height = 185;    // 8
p.hobby  = @"女";  // 8
NSLog(@"%lu - %lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));
//输出40 - 48

class_getInstanceSize源码

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

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

#  define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
    // (x + 7) >> 3 << 3
    return (x + WORD_MASK) & ~WORD_MASK;
}

可见class_getInstanceSize是8字节对齐8(isa)+8(name)+4(age)+8(height)+8(hooby) =36,8字节对齐得出40

malloc_size返回的是calloc之后的大小,我们看看calloc源码

可见calloc源码在libmalloc可在苹果开源官网下载,配置请自行百度

void * calloc(size_t num_items, size_t size) {
    ...
    retval = malloc_zone_calloc(default_zone, num_items, size);
    ...
}
void * malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size){
    ...
    ptr = zone->calloc(zone, num_items, size);
    ...
}
///这里是属性函数递归调用了,打印一下真实调用函数(根据不同的zone调用不同函数)
(lldb) p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $1 = 0x000000010037ea8f (.dylib`default_zone_calloc at malloc.c:249)

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

///这里是属性函数递归调用了,打印一下真实调用函数(根据不同的zone调用不同函数)
(lldb) p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $1 = 0x000000010037ea3a (.dylib`nano_calloc at nano_calloc.c:878)

static void * nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size){
    ...
    void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
    ...
}

static void * _nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested){
    ...
    size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
    ...
}

#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 + 16-1) >> 4 << 4  即 16字节对齐
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;   
    slot_bytes = k << SHIFT_NANO_QUANTUM;							  
    ...
}

可见malloc_size返回的是16字节对齐的大小,对象申请的是40字节,按照16字节对齐,16*3 = 48字节

内存对齐提高了CPU的访问效率,以空间来换取时间; 对象属性按照8字节对齐,系统分配(对象)按照16字节对齐;

Integer data type ILP32 size ILP32 alignment LP64 size LP64 alignment
char 1 byte 1 byte 1 byte 1 byte
BOOL,bool 1 byte 1 byte 1 byte 1 byte
short 2 byte 2 byte 2 byte 2 byte
int 4 byte 4 byte 4 byte 4 byte
long 4 byte 4 byte 8 byte 8 byte
long long 8 byte 4 byte 8 byte 8 byte
pointer 4 byte 4 byte 8 byte 8 byte
size_t 4 byte 4 byte 8 byte 8 byte
NSInteger 4 byte 4 byte 8 byte 8 byte
CFIndex 4 byte 4 byte 8 byte 8 byte
fpos_t 8 byte 4 byte 8 byte 8 byte
off_t 8 byte 4 byte 8 byte 8 byte

isa结构

//联合体位域
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h 位域
    };
#endif
};
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD                                                      \
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

# else
#   error unknown architecture for packed isa
# endif

isa其实是一个union(联合体),而这其实是从内存管理层面来设计的,联合体中所有成员共享内存,联合体内存的大小取决于内部成员大小最大的那个元素,对于 isa 指针来说,就不用额外声明很多的属性,直接在内部的 ISA_BITFIELD(位域) 保存信息。

  1. nonpointer: 表示是否对 isa 指针开启指针优化
    0: 纯 isa 指针
    1: 不止是类对象地址, isa 中包含了类信息、对象的引用计数等
  2. has_assoc: 关联对象标志位,0 没有,1 存在
  3. has_cxx_dtor: 该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
  4. shiftcls: 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
  5. magic: 用于调试器判断当前对象是真的对象还是没有初始化的空间
  6. weakly_referenced: 标志对象是否被指向或者曾经指向一个 ARC 的弱变量, 没有弱引用的对象可以更快释放。
  7. deallocating: 标志对象是否正在释放内存
  8. has_sidetable_rc: 当对象引用技术大于 10 时,则需要借用该变量存储进位
  9. extra_rc: 当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc