iOS-字节对齐、内存对齐

432 阅读3分钟

问题先行:

1、int、bool、float、double、string、指针地址、占用的内存大小是多少?
2、一个无任何属性对象系统开辟内存大小是?
3、一个无任何属性对象实际占用的内存大小是?
4、成员变量为什么是8字节对齐,对象却是16字节对齐?

资料准备:

1、objc源码下载opensource.apple.com/tarballs/ob…
2、libmalloc源码下载opensource.apple.com/tarballs/li…

字节对齐

前情提要:

很多 CPU(如基于 Alpha,IA-64,MIPS,和 SuperH 体系的)拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。所以,如果编译器不进行内存对齐,那在很多平台的上的开发将难以进行。
CPU 不是以字节为单位存取数据的。CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。每次内存存取都会产生一个固定的开销,减少内存存取次数将提升程序的性能。所以 CPU 一般会以 2/4/8/16/32 字节为单位来进行存取操作。我们将上述这些存取单位也就是块大小称为(memory access granularity)内存存取粒度。
在 iOS 开发中编译器会帮我们进行内存对齐。所以这些问题都无需考虑。但如果编译器没有提供这些功能,而且 CPU 也不支持读取非对齐数据,CPU 就会抛出硬件异常交给操作系统处理,从而产生 4610% 的差异。如果 CPU 支持读取非对齐数据,相比对齐数据,你还是要承担额外的开销造成的损失。诚然,这种损失绝不会像 4610% 那么大,但还是不能忽略的。

对象大小内存开辟:

objc对象开辟内存的主流程如下 _class_createInstanceFromZone ->  instanceSize  ->  fastInstanceSize ->  align16

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());
    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;
    size = cls->instanceSize(extraBytes);
    。。。。。
}
size_t instanceSize(size_t extraBytes) const {
    if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
        return cache.fastInstanceSize(extraBytes);
    }
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
}
size_t fastInstanceSize(size_t extra) const
{
    ASSERT(hasFastInstanceSize(extra));
    if (__builtin_constant_p(extra) && extra == 0) {
        return _flags & FAST_CACHE_ALLOC_MASK16;
    } else {
        size_t size = _flags & FAST_CACHE_ALLOC_MASK;
        // remove the FAST_CACHE_ALLOC_DELTA16 that was added
        // by setFastInstanceSize
        return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
    }
}
static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

通过源码可以看到字节对齐的核心方法是align16,对齐算法流程如下

验证:

创建一个无任何属性的对象,打印内存大小为16

@interface ASTest : NSObject
@end
@implementation ASTest
@end
ASTest *test = [[ASTest alloc]init];
NSLog(@"%zd", malloc_size((__bridge const void *)(test)));
/// 输出结果如下
16

获取内存大小:

获取内存大小的方式
1、sizeof()  //获取类型大小
2、class_getInstanceSize()  //获取对象实际的大小,8字节对齐
3、malloc_size() //系统实际开辟内存大小 16字节对齐

sizeof()

ASTest *test = [[ASTest alloc]init];
NSString *string = @"";
NSLog(@"%zd", sizeof(int));    // int类型    占用4个字节
NSLog(@"%zd", sizeof(BOOL));   // Bool类型   占用1个字节
NSLog(@"%zd", sizeof(float));  // float类型  占用4个字节
NSLog(@"%zd", sizeof(double)); // double类型 占用8个字节
NSLog(@"%zd", sizeof(string)); // sting类型  占用8个字节
NSLog(@"%zd", sizeof(char));   // char类型   占用1个字节
NSLog(@"%zd", sizeof(short));  // short类型  占用2个字节

class_getInstanceSize()

源码调用栈如下 class_getInstanceSize -> alignedInstanceSize -> word_align

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}
#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 size_t word_align(size_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

通过上面源码可以看出,内存实际占用大小是进行8字节对齐

@interface ASTest : NSObject
@end
@implementation ASTest
@end
ASTest *test = [[ASTest alloc]init];
NSLog(@"%lu", class_getInstanceSize(ASTest.class));
/// 输出结果如下
8

malloc_size()

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 NANO_REGIME_QUANTA_SIZE  (1 << SHIFT_NANO_QUANTUM)  // 16

通过上面源码可以看出,内存系统开辟大小是进行16字节对齐

内存对齐

内存对齐原则:

1、数据成员对齐规则:
1、结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)
2、结构体大小必须是所有成员大小的整数倍,也即所有成员大小的公倍数。
3、当结构体嵌套的时候,offset是根据嵌套结构体内部的最大成员的大小来计算
2、整个结构体大小
1、最后计算结构体大小的时候,不能把嵌套结构体看成是一个整体,而要拆开来看
结构体内存对齐过程如下

struct MyStruct1 {
    double a;   // 8 [0 8] (0-7)
    char b;     // 1 [8 1] (8)
    int c;      // 4 [9 4] 9非4的倍数:实际(12 13 14 15)
    short d;    // 2 [16 2] (16 17)
}struct1;
// 内部需要的大小为: 17
// 最大属性 : 8
// 结构体大小需要是其内部最大属性的整数倍: 大于17的8的倍数 --> 24

struct MyStruct2 {
    double a;   //8 [0 8] (0-7)
    int c;      //4 [8 4] (8 9 10 11)
    char b;     //1 [12 1] (12)
    short d;    //2 [13 2] 13非2的倍数:实际(14 15)
}struct2;
// 内部需要的大小为: 15
// 最大属性 : 8
// 结构体大小需要是其内部最大属性的整数倍: 大于15的8的倍数 --> 16

struct MyStruct3 {
    double a;   //8 [0 8] (0-7)
    int c;      //4 [8 4] (8 9 10 11)
    char b;     //1 [12 1] (12)
    short d;    //2 [13 2] 13非2的倍数:实际(14 15)
    int e;      //4 [16 4] (16 17 18 19)
    struct MyStruct1 str1; // 24 [20 24] 20非24的倍数:实际(24 47)
    int f;      //4 [48 4] (48 49 50 51)
}struct3;
// 内部需要的大小为: 51
// 最大属性 : 8
// 结构体大小需要是其内部最大属性的整数倍: 大于51的8的倍数 --> 56

在实际苹果系统会为MyStruct1进行如MyStruct2一样的优化,进行了属性重排

问题先行解析:

1、见👆sizeof() 部分
2、见👆malloc_size() 部分
3、见👆class_getInstanceSize() 部分
4、对象开辟的内存如果和成员变量的字节对齐一致的话,可能会导致内存溢出