前言
内存对齐
这个词总是环绕在我们耳边,但是真正的含义我们却一知半解,总感觉被迷雾笼罩,今天就探索下内存对齐,把它神秘的面纱揭开。俗话说实践是检验真理的唯一标准,直接上案例。下面打印对象类型的内存大小,对象实际的内存大小和系统分配的内存大小。代码和打印如下:
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 << 4
16
进制对齐 流程图后面补吧