什么是内存对齐

内存对齐

什么是内存对齐

  • 元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每个元素放置到内存中时,它都会认为内存是按照自己的大小(通常它为4或8)来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始,这就是所谓的内存对齐。

为什么要内存对齐

  • 有些CPU可以访问任意地址上的任意数据,而有些CPU只能在特定地址访问数据,因此不同硬件平台具有差异性,这样的代码就不具有移植性,如果在编译时,将分配的内存进行对齐,这就具有平台可以移植性了
  • CPU每次寻址都是要消费时间的,并且CPU 访问内存时,是以字长(word size)为单位访问,所以数据结构应该尽可能地在自然边界上对齐,如果访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次访问,内存对齐后可以提升性能。这是一种以空间换时间的方法,目的降低cpu开销。
  • 举例:
    • 假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器。这需要做很多工作。
    • 现在有了内存对齐的,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。

内存对齐的规则

每个特定平台上的编译器都有自己的默认"对齐系数",常用平台默认对齐系数如下:(32位系统对齐系数是4,64位系统对齐系数是8)。这只是默认对齐系数,实际上对齐系数我们是可以修改的,程序员可以通过预编译命令#pragma pack(),n = 1, 2, 4, 8, 18来改变这一系数,其中的n就是你要指定的“对齐系数”

  1. 原则一:数据成员对⻬规则
    • 结构体(struct)或者联合体(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如 int 4字节,那么存储位置可以是0-4-8-12-16-20 依次类推)
    • 数据成员的对齐规则可以理解为min(m, n) 的公式, 其中 m表示当前成员的开始位置, n表示当前成员所需要的位数。如果满足条件 m 整除 n (即 m % n == 0), n 从 m 位置开始存储, 反之继续检查m+1 能否整除 n, 直到可以整除, 从而就确定了当前成员的开始位置
  2. 原则二:数据成员为结构体
    • 如果一个结构体里有某些结构体成员,则该结构体成员要从其内部最大元素大小的整数倍地址开始存储,比如struct a里有struct bb里面有char, int, double, short,那么b应该从8的整数倍地址开始存储,即最大成员为double 8字节
  3. 原则三:结构(或联合)的整体对齐规则
    • 在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的整数倍
    • 比如说:编译器指定按照8字节对齐,然后结构体最大数据成员长度为4,最终按照4字节对齐

结构体中内存对齐

  • 根据上面的内存对齐规则,我们探索一下结构体中内存对齐

sizeof

  • sizeof 是一个操作符,不是函数
  • 我们一般使用sizeof计算内存大小时,传入的对象主要是数据类型,这个是在编译阶段就会确定大小而不是运行时。sizeof最终得到的结果是该数据类型占用空间的大小

无嵌套

struct Struct1 {
    double a;
    char b;
    int c;
    short d;
}struct1;

struct Struct2 {
    double a;
    int b;
    char c;
    short d;
}struct2;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%lu-%lu", sizeof(struct1), sizeof(struct2));
    }
    return 0;
}

打印结果:内存对齐[8322:58962] 24-16

  • 根据结果发现,结构体中包含的变量相同,只是因为位置不一样,但是内存大小不一样
  • 根据内存对齐原则,来分析一下结构体中成员的内存占用
  • Struct1中:
    • 变量a: 占8个字节,offset0开始, min(0,8), 即0 ~ 7 存放a
    • 变量b: 占1个字节,offset8开始, min(8,1), 即8 存放d
    • 变量c: 占4个字节,offset9开始, min(9,4)9 % 4 != 0,继续往后移动直到找到可以整除4的位置 1212 ~ 15 存放b
    • 变量d: 占2个字节,offset16开始,min(16,2),即16 ~ 17 存放c
    • 实际占用18个字节,根据内存对齐原则三,结构体中最大变量a占用8字节,所有最终大小必须是8字节的倍数,最终占用24个字节
  • Struct2中:
    • 变量a: 占8个字节,offset0开始, min(0,8), 即0 ~ 7 存放a
    • 变量b: 占4个字节,offset8开始, min(8,4), 即8 ~ 11 存放b
    • 变量c: 占2个字节,offset12开始,min(12,2),即12 ~ 13 存放c
    • 变量d: 占1个字节,offset14开始,min(14,1),即14 存放d
    • 实际占用了15个字节,根据内存对齐原则三,结构体中最大变量a占用8字节,所有最终大小必须是8字节的倍数,最终占用16个字节。

有嵌套

struct Struct2 {
    double a;
    int b;
    char c;
    short d;
}struct2;

struct Struct3 {
    double a;
    char b;
    int c;
    short d;
    struct Struct2 str;
}struct3;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%lu-%lu", sizeof(struct2), sizeof(struct3));
    }
    return 0;
}

打印结果:内存对齐[36173:182032] 16-40

  • Struct3中:
    • 变量a: 占8个字节,offset0开始, min(0,8), 即0 ~ 7 存放a
    • 变量b: 占4个字节,offset8开始, min(8,4), 即8 ~ 11 存放b
    • 变量c: 占2个字节,offset12开始,min(12,2),即12 ~ 13 存放c
    • 变量d: 占1个字节,offset14开始,min(14,1),即14 存放d
    • 变量str,由于它是结构体变量,根据内存对齐规则二:结构体成员要从其内部最大元素大小的整数倍地址开始存储。结构体中最大变量占8字节,所以offset需要从16开始,Struct2的内存大小是18字节。所以min(16, 18),即16-33存放str
    • 实际内存大小是34字节,根据内存对齐原则三,结构体中最大变量a占用8字节,所有最终大小必须是8字节的倍数,最终占用40个字节

iOS中对象内存对齐

iOS中获取内存大小方式

  1. sizeof()在上一小节已经讲述
  2. class_getInstanceSize()获取的是一个对象实际占用的内存空间
  3. malloc_size()获取的是系统实际给开辟的内存空间大小
  • 我们具体看一下后两个实现
class_getInstanceSize()
size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
  • 内部调用了alignedInstanceSize()方法
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}
  1. 我们需要获取未内存对齐大小unalignedInstanceSize()
// May be unaligned depending on class's ivars.
// 可以根据类的成员变量进行对齐。
uint32_t unalignedInstanceSize() const {
    ASSERT(isRealized());
    return data()->ro()->instanceSize;
}
  • 该方法内部是获取类的成员变量大小
  1. 对获取到的内存大小,进行对齐
#define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
  • 此处采用的是8字节对齐,也就是说对象中成员变量按照8字节对齐。
  • 具体计算过程如下:假设x为8字节
    1. (x + WORD_MASK) = (8 + 7) = 15,用二进制表示:0000 1111
    2. ~WORD_MASK = ~7,用二进制表示:1111 1000
    3. 15 & ~7,用二进制表示:0000 1000
    4. 最终结果8个字节
malloc_size()
  • 获取的是系统实际给开辟的内存空间大小,采用了16字节对齐。

iOS中内存对齐

实际占用内存对齐方式
  • 通过class_getInstanceSize()方法,我们可以看到内部是通过8字节对齐
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
系统分配内存对齐方式
  • 调用系统分配的流程可以查看Runtime源码解析-alloc,里面有完整的alloc流程
  • 这里我们只需要知道,最终系统分配内存时,调用instanceSize方法来获取大小。
inline 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;
}
  • 这里一般都会进入fastInstanceSize方法
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字节对齐。该align16()方法和word_align()内部实现类似,具体如何16字节对齐请参考前面。
问题
  1. 为什么实际内存对齐才用8字节?
    1. 在iOS对象中,由于任何类都继承NSObject,然后都会有8字节长的isa_t指针,所以根据内存对齐规则,iOS对象才用了8字节对齐。
  2. 为什么系统开辟不采用8字节,而选择16字节?
    1. 内存容错:对于一个没有任何成员变量的对象来说,它的大小是8字节(isa_t指针)。如果多个没有成员变量的对象存在一起,就会出现每个对象isa_ta指针挨着情况,没有容错性。而才用16字节对齐,之间有8字节的容错性,可有利于对象的扩展。
    2. 读取速度快:以16字节的方式去读取,可以加快它的读取速度,提高cpu利用率。

内存优化

  • 由于iOS中对象的本质就是结构体,那么我们可以说对象的内存对齐就是结构体的内存对齐么?我们通过例子具体来探究一下

  • 先定一个Test类,然后Test类里面的成员变量和Struct1保持一致

@interface Test : NSObject

@property (nonatomic, assign) double a;
@property (nonatomic, assign) char b;
@property (nonatomic, assign) int c;
@property (nonatomic, assign) short d;

@end

@implementation Test

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Test *test = [Test alloc];
        test.a = 10.0;
        test.b = 'a';
        test.c = 12;
        test.d = 100;
        
        NSLog(@"struct1 = %lu Test = %lu",sizeof(struct1), class_getInstanceSize([test class]));
    }
    return 0;
}

打印结果:内存对齐[42277:218736] struct1 = 24 Test = 24

  • Test类中定义的变量和struct2中顺序是一样的,但是test对象会多带一个isa_t变量,该变量占8个字节。打印结果显示它们的大小是一样的,说明test对象我们定义的变量直占了16个字节。明明和结构体顺序、类型都一样的,为什么对象占用的内存和结构体却不一样。这是因为苹果的内存优化机制

  • 我们具体看一下它是如何进行内存优化的:

  • 通过lldb断点打印可以看出,a的读取是通过0x4024000000000000b的读取是通过0x0061c的读取是通过0x0000000cd的读取是通过0x0064。我们可以发现char b; int c; short d;共用了一个8字节空间。
  • 苹果采用时间换空间的方式,通过对对象的属性存储顺序进行重排,达到内存优化的目的。
总结
  • 为了提高cpu的存储效率和安全访问,制定了内存对齐规则。但是也因为内存对齐,从而浪费了很多内存。苹果为了内存优化尽可能降低内存的浪费,使用了属性重排的方式。
  • 既保证了存储的效率,又减少了内存的浪费。
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值