02 关于内存对齐

我们先来看一个例子

例子

在例子中有一个struct1结构体,内部有一个double类型和一个int类型的数据,按理来说double类型8字节,int类型4字节,struct1结构体的大小应该是12字节才对,但是我们通过sizeof函数打印获取struct1的占用的内存大小时得到的却是16字节,多花销的4字节的内存就是内存对齐导致的。

目录

  • 什么是内存对齐
  • 为什么要内存对齐
  • 内存对齐的规则

什么是内存对齐

计算机中内存空间是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是,在访问特定类型变量的时候通常在特定的内存地址访问,这就需要对这些数据在内存中存放的位置有限制,各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排列,这就是对齐

内存对齐是编译器的管辖范围。表现为:编译器为程序中的的每个"数据单元"安排在合适的位置上。

为什么要内存对齐

为了解释这个问题,我们要先了解一下处理器是如何读取内存的。

如果我们吧内存看做是简单的字节数组,比如在C语言中,char *就可以表示一块内存。那么或许我们会认为,它的内存读取方式可以按照1byte顺序读取,如下图

单字节存取

然而,尽管内存是以字节为单位,但是大部分处理器并不是按照字节块来存取内存的,这取决于数据类型处理器的设置;它一般会以2字节4字节8字节16字节甚至是32字节的块来存取内存,我们将上述这些存取单位称为内存存取粒度

4字节存取

现在我们知道计算机的处理器是以一定大小的块来进行内存读取的,这作为我们的前提条件,那么为了解释为什么要内存对齐,我们不妨先看一看不对齐的情况会出现什么问题。

对齐和数据在内存中的位置有关。如果一个变量的内存地址刚好位于它本身长度的整数倍的位置,他就被称作自然对齐。例如一个整型变量(占4个字节)的地址为0x16,那它就是自然对齐的。

现在假设一个整型变量(4字节)不是自然对齐的,它的起始地址落在0x02位置,处理器想要访问它的值,按照4字节的块进行读取,从0x00起读,读取4字节大小,到0x03,这样的一次读取之后我们并不能取到我们要访问的整型数据,紧接着处理器会继续再往下读4字节,从0x04开始到0x07为止,到这里处理器才能读取到了我们需要访问的内存数据,当然这中间还存在剔除与合并的过程。

非对齐读取数据

所以在此例子中,当整型变量起始地址落在0x2时,处理器需要2次读取才能取到我们要访问的内容,那如果数据时内存对齐的呢?
内存对齐读取数据

显然,如果数据时内存对齐的,对于本例仅需读取1次便可以读取到目标数据。可见内存对齐与否会影响到处理器的存取效率。同时:

各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取,而不是内存中任意地址都是可以存取的。

比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证内存对齐。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据进行对齐,会在存取效率上带来损失

也正是由于只能在特定的地址处存取数据,所有在访问一些数据时,对于访问未对齐的内存,处理器可能需要进行多次访问;而对于对齐的内存,只需要访问一次就可以。这就是为什么要进行内存对齐的原因。

内存对齐的原则

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

在上面的例子中我们创建了一个结构体,同时打印了它的内存大小为24,那这个24是如何计算得来的呢?

struct1内存分布

按照规则a4个字节的存储空间,同时从0x0的位置开始,所以0x0到0x4的位置存储a的数据,b8个字节的存储空间,本来应该从0x4开始,但由于0x4不是8的整数倍,故0x4到0x7的位置空出,从0x8开始存储b,一直到0xfc2个字节的存储空间,0x10正好可以被2整除,故c存放在0x10和0x11的位置,同时根据规则3,结构体的大小需要是内部最大数据整数倍,故struct1的大小应该是8的倍数,故0x12到0x17补空。至此整个struct1的大小为0x0到0x17,共计24个字节。

struct2内存大小

在struct2中我们将struct1中a和b的位置互换了一下,结果却得到struct2的内存大小只有16个字节,此时我们再来看下struct2的内存分布。

struct2内存分布

struct2中我们首先从0x0开始存放b,占用8个字节0x7,接下来存放a,由于0x8正好可以被4整除,a便存储在0x8到0xb的位置上,c占用2个字节同时0xc正好可以被2整除,c便存储在0xc和0xd的位置上,同时根据规则3,结构体的大小需要是内部最大数据整数倍,故struct2的大小应该是8的倍数,故0xe和0xf补空。至此整个struct2的大小为0x0到0xf,共计16个字节。

结构体嵌套内存大小

在struct3中我们将SLStruct2类型的结构体当做一个结构体数据,得到的结果是需要40个字节的内存空间。此时我们来看下struct3的内存分布情况。

struct3内存分布

struct30x0到0xd的内存分布和struct2的内存分布一样,接下来的数据stru2SLStruct2类型的,故其起始位置需要是SLStruct2中最大数据类型的整数倍,即double类型(8字节)的整数倍,0xe和0xf不符合要求,故补空后从0x10开始存放stru2stru2的内存分布和struct2的内存分布完全一样,占用从0x10到0x1f位置16个字节的空间,然后d需要2个字节的空间并且0x20正好可以被2整除,d占用0x20和0x21两个位置,同时根据规则3,结构体的大小需要是内部最大数据整数倍(注意这里即使嵌套结构体,也只需计算结构体内部最大数据的大小的整数倍,不需计算整个结构体的大小),故struct3的大小应该是8的倍数,故0x22到0x27补空。至此整个struct3的大小为0x0到0x27,共计40个字节。

以上几个例子应该可以帮助我们更好的理解内存对齐的规则。

其实内存对齐就是制定了一套规则以合理的利用内存空间并提高内存访问效率。编译器通过适当增加padding,是每个成员的方位都在一个指令里完成,而不需要多次访问再拼接。是一个以空间换时间的过程。

从以上的例子也可以看出结构体占用的内存大小和数据成员的排列顺序有关,最大的数据成员放在前面可以有效的节约内存。但是在实际书写代码的时候不可能按照数据成员的占用大小来申明属性,如果随便写的话可能会造成大量的内存浪费,那苹果又是如何解决这个问题的呢?

SLPerson

person

在上图中我们创建了一个SLPerson类型的对象,同时给person的各个属性赋值,按照SLPerson类的声明来看各个属性的声明是没有规则的,内存的浪费应该很大。但是当我们将程序运行起来之后打印person的内存分布时发现iOS帮助我们将属性进行了重新排列,将agesex属性存在了0x600001574608开始的8个字节内,将heightweight属性存在了0x600001574610开始的8个字节内,nameengName各占8个字节,内存被有效利用了起来。以上就是苹果帮助我们进行的内存优化,即属性重排。

总结
苹果的内存管理不仅仅有内存对齐,还会通过属性重排的方式帮助我们优化内存,减小内存的开销。

最后我们看下苹果获取内存大小的三种方式:sizeofclass_getInstanceSizemalloc_size

sizeof
sizeof传入的主要对像是数据类型,最终得到的是该数据类型占用空间的大小(如intdoublestruct),如果传入的是实例对象,其对象类型的本质就是一个结构体(即struct objc_object)的指针,所以sizeof返回的是对象指针的大小,而一个对象指针的大小固定是8个字节

class_getInstanceSize
class_getInstanceSize是runtime提供的api,用于获取类的实例对象所占用的内存大小(8字节对齐),并返回具体的字节数,其本质就是获取实例对象中成员变量的内存大小

malloc_size
malloc_size是获取系统实际分配的内存大小(16字节对齐)

例如,我们申明如下SLPerson类:

@interface SLPerson : NSObject

@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *name;

@end

在main函数中创建SLPerson的示例对象person,并通过三种方式获取person的内存大小

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

#import <objc/runtime.h>
#import <malloc/malloc.h>

#import "SLTeacher.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        SLPerson *person = [[SLPerson alloc] init];
        NSLog(@"sizeof(person) = %lu", sizeof(person));
        NSLog(@"class_getInstanceSize(person) = %lu", class_getInstanceSize([person class]));
        NSLog(@"malloc_size(person) = %lu", malloc_size((__bridge const void *)(person)));
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

打印结果如下

2021-01-21 15:29:30.252973+0800 test[3449:221231] sizeof(person) = 8
2021-01-21 15:29:30.253448+0800 test[3449:221231] class_getInstanceSize(person) = 24
2021-01-21 15:29:30.253540+0800 test[3449:221231] malloc_size(person) = 32

由打印可看到
sizeof(person)获取到的是一个指针的大小8字节

class_getInstanceClass([person class])获取到的是SLPerson类型的对象实际需要的内存大小,即其属性所需的内存大小isa(8字节)+name(8字节)+age(4字节)+8字节内存对齐,共需24字节,

malloc_size(person)获取到的是系统实际分配给person对象的内存大小,iOS中系统的内存对齐方式为16字节对齐,即系统每次访问内存的内存存取粒度为16字节,故在24字节的基础上又补了8字节的空位,共计32字节。

本文参考自:关于内存对齐,看我

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容