前言
内存对齐这个词总是环绕在我们耳边,但是真正的含义我们却一知半解,总感觉被迷雾笼罩,今天就探索下内存对齐,把它神秘的面纱揭开。俗话说实践是检验真理的唯一标准,直接上案例。下面打印对象类型的内存大小,对象实际的内存大小和系统分配的内存大小。代码和打印如下:
LWPerson * person = [LWPerson alloc];
person.name = @"person";
person.age = 18;
LWPerson * newPerson;
NSLog(@"对象类型的内存大小--%lu",sizeof(person));
NSLog(@"对象实际的内存大小--%lu",class_getInstanceSize([person class]));
NSLog(@"系统分配的内存大小--%lu",malloc_size((__bridge const void *)(person)));
NSLog(@"==================");
NSLog(@"对象类型的内存大小--%lu",sizeof(newPerson));
NSLog(@"对象实际的内存大小--%lu",class_getInstanceSize([newPerson class]));
NSLog(@"系统分配的内存大小--%lu",malloc_size((__bridge const void *)(newPerson)));
2021-06-08 11:16:28.097465+0800 alignStyle[73542:9731629] 对象类型的内存大小--8
2021-06-08 11:16:28.097520+0800 alignStyle[73542:9731629] 对象实际的内存大小--24
2021-06-08 11:16:28.097562+0800 alignStyle[73542:9731629] 系统分配的内存大小--32
2021-06-08 11:16:28.097583+0800 alignStyle[73542:9731629] ==================
2021-06-08 11:16:28.097607+0800 alignStyle[73542:9731629] 对象类型的内存大小--8
2021-06-08 11:16:28.097629+0800 alignStyle[73542:9731629] 对象实际的内存大小--0
2021-06-08 11:16:28.097649+0800 alignStyle[73542:9731629] 系统分配的内存大小--0
结果分析:
sizeof:对象类型的内存大小,sizeof是一个操作符号,不是函数。计算的是传进来的数据类型的大小,这个在编译时期就已经确定。所以sizeof(person)和sizeof(newPerson)都是8字节,因为它们的本质是结构体指针。class_getInstanceSize:对象实际的内存大小,内存大小是由类的成员变量的大小决定。实际上并不是严格意义上的对象的内存的大小,因为内存进行了8字节对齐,核心算法是define WORD_MASK 7UL ((x + WORD_MASK) & ~WORD_MASK。 所以person的内存大小是24而不是20,newPerson只是声明了一个变量,并没有开辟内存,所以大小是0。malloc_size系统分配的内存大小是按16字节对齐的方式,即分配的大小是16的倍数 ,不足16的倍数系统会自动填充字节,注意系统的16字节对齐是在实际的内存大小(经过8字节对齐)的基础上。
问题:
class_getInstanceSize和malloc_size底层做了什么? 你怎么知道
class_getInstanceSize是8字节对齐的呢? 而malloc_size是16字节对齐的呢?class_getInstanceSize和malloc_size探索流程会放在文章的结尾。
内存对齐
前言我们发现对象实际的内存大小是8字节对齐,那么到底是怎么对齐的。引出我们的重点内存对齐。
各类型所占字节
在这里整理些基本数据类型在不同系统下的字节大小,方便大家查看。
需要内存对齐的原因
- 内存是以字节为基本单位,
cpu在存取数据时,是以块为单位存取,并不是以字节为单位存取。频繁存取未对齐的数据,会极大降低cpu的性能。字节对齐后,会减低cpu的存取次数,这种以空间换时间的做法目的降低cpu的开销。 cpu存取是以块为单位,存取未对齐的数据可能开始在上一个内存块,结束在另一个内存块。这样中间可能要经过复杂运算在合并在一起,降低了效率。字节对齐后,提高了cpu的访问速率。
内存对齐原则
-
数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在
offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在 32 位机为4字节,则要从4的整数倍地址开始存储。 -
结构体作为成员:如果一个结构里有某些结构体成员,
则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.) -
收尾工作:结构体的总大小,也就是
sizeof的结果,必须是其内部最大成员的整数倍.不足的要补齐。
内存对齐原则描述的那么复杂,搞我心态是吧,必须实例解我疑惑
结构体内存对齐(无嵌套)
对象的本质就是结构体,对象的底层实现是结构体。内存对齐实际上可以看做是结构体内存对齐,只不过系统对实例化的对象进行了内存优化。接下来实例探究下结构体内存对齐
struct LWStruct1{
double a; // 8
int b; // 4
short c; // 2
char d; // 1
}LWStruct1;
struct LWStruct2{
double a; // 8
char d; // 1
int b; // 4
short c; // 2
}LWStruct2;
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"-----%lu-----%lu",sizeof(LWStruct1),sizeof(LWStruct2));
}
return 0;
}
2021-06-08 15:02:54.392903+0800 alignStyle[74021:9798038] -----16-----24
结果分析发现 LWStruct1 和 LWStruct2 所包含的变量是一样的,只是位置不一样,但是内存大小不一样,为什么? 这就是结构体内存对齐。
下面就根据内存对齐原则进行简单的计算和分析
LWStruct1内存大小详细过程(min(m,n) m表示当前开始的位置,n表示大小)
- 变量
a: 占8个字节,offert从0开始,min(0,8), 即0 ~ 7存放a - 变量
b: 占4个字节,offert从8开始,min(8,4), 即8 ~ 11存放b - 变量
c: 占2个字节,offert从12开始,min(12,2),即12 ~ 13存放c - 变量
d: 占1个字节,offert从14开始,min(14,1),即14存放d结果显示LWStruct1的实际的内存大小是15字节,LWStruct1中最大的变量是a占个8字节。所以LWStruct1的实际内存大小必须是8的整数倍,15不是8的整数倍,向上取整,不足的自动补齐为16字节。最后LWStruct1的内存大小为16字节。
LWStruct1解析图如下
LWStruct2内存大小详细过程
- 变量
a: 占8个字节,offert从0开始,min(0,8), 即0 ~ 7存放a - 变量
d: 占1个字节,offert从8开始,min(8,1), 即8存放d - 变量
b: 占4个字节,offert从9开始,min(9,4),9 % 4 != 0,继续往后移动直到找到可以整除4的位置12即12 ~ 15存放b - 变量
c: 占2个字节,offert从16开始,min(16,2),即16 ~ 17存放c结果显示LWStruct2的实际的内存大小是18字节,LWStruct2中最大的变量是a占个8字节。所以LWStruct2的实际内存大小必须是8的整数倍,18不是8的整数倍,向上取整,不足的自动补齐为24字节。最后LWStruct2的内存大小为24字节。
LWStruct2解析图如下
结构体中嵌套结构体
struct LWStruct1{
double a; // 8
int b; // 4
short c; // 2
char d; // 1
}LWStruct1;
struct LWStruct2{
double a; // 8
char d; // 1
int b; // 4
short c; // 2
}LWStruct2;
struct LWStruct3{
long a; // 8
int b; // 4
short c; // 2
char d; // 1
struct LWStruct2 lwStr;
}LWStruct3;
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"-----%lu-----%lu----%lu",sizeof(LWStruct1),sizeof(LWStruct2),sizeof(LWStruct3));
}
return 0;
}
2021-06-08 16:28:40.819854+0800 alignStyle[74082:9819949] -----16-----24----40
LWStruct3内存大小详细过程
- 变量
a: 占8个字节,offert从0开始,min(0,8), 即0 ~ 7存放a - 变量
b: 占4个字节,offert从8开始,min(8,4), 即8 ~ 11存放b - 变量
c: 占2个字节,offert从12开始,min(12,2),即12 ~ 13存放c - 变量
d: 占1个字节,offert从14开始,min(14,1),即14存放d - 变量
lwStr:lwStr是结构体变量,内存对齐原则结构体成员要从其内部最大元素大小的整数倍地址开始存储。LWStruct2中的最大的变量占8字节,所以offert从16开始,LWStruct2的内存大小是18字节。min(16,18),即18 ~ 33存放lwStr
结果显示 LWStruct3 的实际的内存大小是34字节,LWStruct3中最大的变量是lwStr和 a都是 8 字节。所以LWStruct3的实际内存大小必须是8的整数倍,34不是8的整数倍,向上取整,不足的自动补齐为40字节。最后LWStruct3的内存大小为40字节。
LWStruct3解析图如下
内存优化
结构体内存根据变量位置的顺序最后内存的大小可能不一样,实例化对象的内存对齐会不会也会出现这种情况呢。那就探究下呗
LWPerson * person = [LWPerson alloc];
person.a = 100.0;
person.b = 'a';
person.c = 10;
person.d = 2;
NSLog(@"----%lu",class_getInstanceSize([person class]));
2021-06-08 17:11:27.743037+0800 alignStyle[74104:9830701] ----24
LWPerson 中自定义的变量和 LWStruct2 的变量的顺序是一模模一样样,但是对象自带一个变量isa指针占8字节。所以LWPerson 中自定义的变量占了16个字节,奇怪了啊,结构体的顺序,名字都是一样的啊。这是为什么呢?这就是下面要说的内存优化(系统多机智啊浪费那么多内存,当然得优化啊),具体看看是怎么优化的
通过lldb断点打印可以看出 ,a的读取通过 0x4059000000000000,b的读取通过0x61(a的ASCII码是97),c的读取通过 0x0a ,d的读取通过0x02,
神奇的发现 char b,int c,short d,共用了一个8字节内存空间,对象的属性或者变量存储顺序和结构体的也不一样进行了重排。系统进行了内存优化。
总结
内存对齐有制定了一套规则,目的是提高cpu的存取效率和安全的访问。字节对齐可能浪费了部分内存,但是同时进行内存优化尽可能的降低了内存的浪费,即保证了存取的速率,又减少了内存的浪费,不得不说真的很优秀啊。
补充
class_getInstanceSize 探究
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
进入 alignedInstanceSize
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
进入 word_align
#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 uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
总结:word_align多么熟悉的词,就不多说了好吧。
malloc_size 探究
malloc_size 想进去看看里面具体是怎么实现的,但是点击去如下图
malloc_size方法实现没有提供,怎么办呢?看文件路径可能在malloc库,下载libSystem_malloc库编译运行,malloc_size获取系统分配的内存大小
calloc是系统开辟内存,当然大小肯定也会计算分配,所以我们探究calloc方法
void *p = calloc(1, 40);
进入 calloc
calloc(size_t num_items, size_t size)
{
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}
进入 _malloc_zone_calloc
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
malloc_zone_options_t mzo)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
if (malloc_check_start) {
internal_check();
}
//这是关键,别问,问就是一下就看到了
ptr = zone->calloc(zone, num_items, size);
if (os_unlikely(malloc_logger)) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
}
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
if (os_unlikely(ptr == NULL)) {
malloc_set_errno_fast(mzo, ENOMEM);
}
return ptr;
}
进入 zone->calloc此时发现点不进去,怎么办呢?我用的是汇编方法,直接看汇编
全局搜索 default_zone_calloc
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->calloc此时发现又点不进去,老办法看汇编
全局搜索 nano_calloc
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
size_t total_bytes;
if (calloc_get_size(num_items, size, 0, &total_bytes)) {
return NULL;
}
if (total_bytes <= NANO_MAX_SIZE) {
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
return p;
} else {
/* FALLTHROUGH to helper zone */
}
}
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
return zone->calloc(zone, 1, total_bytes);
}
进入 _nano_malloc_check_clear 因为我们现在只关心内存的大小所以直接看size_t
进入 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 + 16 - 1) >> 4 左移4位
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
// round up and shift for number of quanta
// slot_bytes = k << 4 右移4位
slot_bytes = k << SHIFT_NANO_QUANTUM;
// multiply by power of two quanta size
*pKey = k - 1;
// Zero-based!
return slot_bytes;
}
总结
k >> 4 k << 416进制对齐 流程图后面补吧