CPU 存取原理

一、“存”示例
  • CPU 并不是以字节为单位存取数据的。CPU 把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此 CPU 在读取内存时是一块一块进行读取的。每次内存存取都会产生一个固定的开销,减少内存存取次数将提升程序的性能
  • CPU 一般会以 2/4/8/16/32 字节为单位来进行存取操作,将这些存取单位也就是块大小称为(memory access granularity)内存存取粒度
  • CPU 的数据总线宽度决定了 CPU 对数据的吞吐量。
  • 64 位 CPU 一次处理64 bit也就是8个字节的数据,32位同理,每次处理4个字节的数据。
    • 假设有以下数据:

  • 那么 CPU 存储之后如下:

二、“取”示例
  • 在一个存取粒度为 4 字节的内存中,先从地址 0 读取 4 个字节到寄存器,然后从地址 1 读取 4 个字节到寄存器:
    • 当从地址 0 开始读取数据时,是读取对齐地址的数据,直接通过一次读取就能完成;当从地址 1 读取数据时读取的是非对齐地址的数据,需要读取两次数据才能完成。

  • 在读取完两次数据后,还要将 0-3 的数据向上偏移 1 字节,将 4-7 的数据向下偏移 3 字节,最后再将两块数据合并放入寄存器。

  • 对一个内存未对齐的数据进行了这么多额外的操作,这对 CPU 的开销很大,大大降低了 CPU 性能。

内存对齐简介

一、概念
① 什么是内存对齐?
  • 计算机内存都是以字节为单位划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8的倍数),这就是所谓的内存对齐
  • 内存对齐是一种在计算机内存中排列数据(表现为变量的地址) 、访问数据(表现为CPU读取数据)的一种方式。
  • 内存对齐包含了两种相互独立又相互关联的部分:基本数据对齐和结构体数据对齐
② 为什么要进行内存对齐?
  • 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
③ 内存对齐原则
  • 在 iOS 中,对象的属性需要进行内存对齐,而对象本身也需要进行内存对齐。内存对齐有三原则:
    • 数据成员对齐原则: 结构( struct )(或联合( union ))的数据成员,第一个数据成员放在 offset 为 0 的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小
    • 结构体作为成员: 如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(如:struct a ⾥存有struct b,b⾥有char、int 、double 等元素,那b应该从8的整数倍开始存储)
    • 收尾工作: 结构体的总大小,也就是 sizeof 的结果,必须是其内部最大成员的整数倍,不足的要补⻬
  • 简而言之:
    • 前面的地址必须是后面的地址正数倍,不是就补齐;
    • 结构体里面的嵌套结构体大小要以该嵌套结构体最大元素大小的整数倍;
    • 整个 Struct 的地址必须是最大字节的整数倍。
④ 获取内存大小的方式
  • sizeOf:
    • sizeof 是一个操作符,不是函数;
    • sizeof 计算内存大小时,传入的主要是对象或者数据类型,在编译器的编译阶段大小就已经确定;
    • sizeof 计算出的结果,是表示占用空间的大小;
  • class_getInstanceSize:class_getInstanceSize runtime 提供的 api,用于获取类实例对象所占内存大小,本质也是计算该实例中所有成员变量的内存大小。
  • malloc_size:malloc_size 获取系统实际分配的内存大小。
⑤ 特别说明
  • 在字节对齐算法中,对齐的主要是对象,而对象的本质则是一个 struct objc_object 的结构体;
  • 结构体在内存中是连续存放的,所以可以利用这点对结构体进行强转;
  • 苹果早期是8字节对齐,现在是16字节对齐。
二、C/OC 基本数据类型的内存大小(字节)
C OC 32位 64位
bool BOOL(64位) 1 1
signed char (_signed char)int8_t、BOOL(32位) 1 1
unsigned char Boolean 1 1
short int16_t 2 2
unsigned short unichar 2 2
int、int32_t NSInteger(32位)、boolean_t(32位) 4 4
unsigned int NSUInteger(32位)、boolean_t(64位) 4 4
long NSInteger(64位) 4 8
unsigned long NSUInteger(64位) 4 8
long long int64_t 8 8
float CGFloat(32位) 4 4
double CGFloat(64位) 8 8

结构体的内存对齐

一、常规结构体
  • 定义一个结构体如下:
typedef struct YDWTeacher {char name;bool sex;int age;float height;double level;
}teacher;
  • 通过上文中的“C/OC 基本数据类型的内存大小”表中可以查看:YDWTeacher这个结构体中的每一个变量占据的内存如下:
    • char name:1字节;
    • bool sex:1字节;
    • int age:4字节;
    • float height:4字节;
    • double level: 8字节;
typedef struct YDWTeacher {char name;    // 1字节bool sex;     // 1字节int age;      // 4字节float height;  // 4字节double level; // 8字节
}teacher;
  • 打印NSLog(@"%lu",sizeof(teacher));如下:
 iOS之内存对齐[25483:1817236] 24
  • 那么,YDWTeacher在系统内存中的存储地址如下(对齐系数默认为成员最大元素大小):

  • 内存分析如下:
 char name => [0], offset = 1;bool sex 长度为 1 个字节,此时 offset 需要 +3,才是 4 的倍数,offset = 4, b => [4, 4], offset = 5, 需要 + 3 = 8;int age 长度为 4 个字节, offset = 8 满足,因此 int age => [8, 11], offset = 12float height 长度为 4 个字节, offset = 12 满足,因此 float height => [12,15], offset = 16;double level 长度为 8 个字节,offset = 16 满足 8 的倍数,因此 double score => [16,23];此时共占用字节 24,由于当前结构体中,最长数据类型 sizeof(double) = 8, 24 是 8 的倍数,因此对齐后取值为 24
二、嵌套结构体
  • 定义一个结构体如下:
typedef struct YDWStudent {char name;      // 1字节int age;        // 4字节bool sex;       // 1字节double score;   // 8字节
}student;
  • 打印NSLog(@"%lu",sizeof(student));如下:
 iOS之内存对齐[25751:1843819] 24
  • 那么,YDWStudent在系统内存中的存储地址如下(对齐系数默认为成员最大元素大小):

  • 内存分析如下:
 char name => [0], offset = 1int age 长度为 4 个字节,此时 offset 需要 +3,才是4的倍数,offset = 4, b => [4, 7], offset = 8bool sex 长度为 1 个字节, offset = 8 满足,因此 c => [8], offset = 9double score 长度为 8 个字节,offset + 7 = 16 才是 8 的倍数,因此 d => [16,23]此时共占用字节 24,由于当前结构体中,最长数据类型 sizeof(double) = 8, 24 是 8 的倍数,因此对齐后取值为 24
  • 接下来,我们就将这两个结构体嵌套:
typedef struct YDWTeacher {char name;    // 1字节bool sex;     // 1字节int age;      // 4字节float height;  // 4字节double level; // 8字节
}teacher;typedef struct YDWStudent {char name;      // 1字节int age;        // 4字节bool sex;       // 1字节double score;   // 8字节teacher *teacher;
}student;
  • 此时NSLog(@"%lu",sizeof(student));变成了:
 iOS之内存对齐[25862:1856001] 32
  • 那么此时,YDWStudent在系统内存中的存储地址如下(对齐系数默认为成员最大元素大小):

  • 内存分析如下:
 char name => [0], offset = 1int age 长度为 4 个字节,此时 offset 需要 +3,才是4的倍数,offset = 4, b => [4, 7], offset = 8bool sex 长度为 1 个字节, offset = 8 满足,因此 c => [8], offset = 9double score 长度为 8 个字节,offset + 7 = 16 才是 8 的倍数,因此 d => [16,23]teacher *teacher 长度为 24 字节,但是 teacher 中最长数据类型为 sizeof(double) = 8, 此时 offset = 24 正好是 8 的倍数, teacher => [24, 31], offset = 32此时共占用字节 32,由于当前结构体中,最长数据类型 sizeof(double) = 8, 32 是 8 的倍数,因此对齐后取值为 32

OC对象申请内存和系统开辟内存

  • 在Xcode中导入#import <objc/runtime.h>与#import <malloc/malloc.h>,然后打印以下函数(获取内存大小):

    • sizeof:即编译时确定大小,获得该数据类型占用空间的大小;
    • class_getInstanceSize:获取类的实例对象所占用的内存大小;
    • malloc_size:获取系统实际分配的内存大小;
 YDWBoy *boy = [YDWBoy alloc];boy.name = @"YDW";boy.nickName = "handsome";boy.age = 18;boy.height = 175.0;NSLog(@"%lu - %lu - %lu", sizeof(boy), class_getInstanceSize([YDWBoy class]), malloc_size((__bridge const void *)(boy)));
  • 得到对应的内存大小字节数,结果如下:
 2020-09-08 00:00:58.621860+0800 iOS之内存对齐[26116:1877306] 8 - 40 - 48
  • 可以发现对象自己申请的内存大小与系统实际给开辟的大小时不一样的,这里对象申请的内存大小是 40 个字节,而系统开辟的是 48 个字节。
  • 40 个字节不难理解,是因为当前对象 boy 有 4 个属性,有三个属性为 8 个字节,有一个属性为 4个字节,再加上 isa 的 8 个字节,就是 32 + 4 = 36 个字节,然后根据内存对齐原则,36 不能被 8 整除,36 往后移动刚好到了 40 就是 8 的倍数,所以内存大小为 40。
  • class_getInstanceSize 和 malloc_size 对同一个对象返回的结果不一样的,原因是 malloc_size 是直接返回的 calloc 之后的指针的大小。
size_t instanceSize(size_t extraBytes) {size_t size = alignedInstanceSize() + extraBytes;// CF requires all objects be at least 16 bytes.if (size < 16) size = 16;return size;
}
  • 通过instanceSize计算的内存大小,向内存中申请 大小 为 size的内存,并赋值给obj,因此 obj是指向内存地址的指针;在未执行 calloc 时,po obj 为 nil,执行后,再 po obj ,返回一个16进制的地址。
 obj = (id)calloc(1, size);
  • 而 class_getInstanceSize 内部实现是:也就是说 class_getInstanceSize 会输出 8 个字节,malloc_size 会输出 16 个字节,当然前提是该对象没有任何属性。
size_t class_getInstanceSize(Class cls) {if (!cls) return 0;return cls->alignedInstanceSize();
}

calloc 的内存对齐

  • 从 calloc 函数出发,虽然不能直接在 libObjc 的源码中找到其对应实现,但是通过观察 Xcode 可以去找 libMalloc 源码。

  • libObjc 和 libMalloc 是相互独立的,所以在 libMalloc 源码中,没必要去走 calloc 前面的流程了,可以通过断点调试 libObjc 源码,发现第二个参数是 40: (这是因为当前发送 alloc 消息的对象有 4 个属性,每个属性 8 个字节,再加上 isa 的 8 个字节,所以就是 40 个字节)

  • 打开 libMalloc 的源码,在新建的 target 中直接手动声明如下的代码:
 void *p = calloc(1, 40);NSLog(@"%lu",malloc_size(p));
  • 运行之后,会有提示以下错误:

  • 直接 Command + Shift + F 进行全局搜索对应的符号,但是会发现找不到,我们再仔细观察,这些符号都是位于 .o 文件里面的,所以我们可以去掉符号前面的下划线再进行搜索,这个时候就可以把对应的代码注释然后重新运行了。运行之后一直沿着源码断点下去,会来到这么一段代码:
 ptr = zone->calloc(zone, num_items, size);
  • 如果直接去找 calloc,就会递归了,所以需要点进去,然后就会发现一个很复杂的东西出现了:

  • 可以直接在断点处使用 LLDB 命令打印这行代码来看具体实现是位于哪个文件中:
 p zone->calloc输出: (void *(*)(_malloc_zone_t *, size_t, size_t)) $1 = 0x00000001003839c7 (.dylib`default_zone_calloc at malloc.c:249)
  • 也就是说 zone->alloc 的真正实现是在 malloc.c 源文件的249行处;
static void *
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,接着再次使用 LLDB 打印内存地址:
 p zone->calloc输出: (void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x0000000100384faa (.dylib`nano_calloc at nano_malloc.c:884)
  • 再次来到 nano_calloc 方法:
static void *
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 里面继续走,然后再次发现 _nano_malloc_check_clear 里面内容非常多,此时要明确一点,目的是找出 48 是怎么算出来的,经过分析之后,我们来到 segregated_size_to_fit:
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{// size = 40size_t k, slot_bytes;if (0 == size) {size = NANO_REGIME_QUANTA_SIZE; // Historical behavior}// 40 + 16-1 >> 4 << 4// 40 - 16*3 = 48//// 16k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quantaslot_bytes = k << SHIFT_NANO_QUANTUM;                            // multiply by power of two quanta size*pKey = k - 1;                                                    // Zero-based!return slot_bytes;
}
  • 这里就可以看出:进行的是 16 字节对齐,那么也就是说传入的 size 是 40,在经过 (40 + 16 - 1) >> 4 << 4 操作后,结果为48,也就是16的整数倍。
  • 最后得出结论:
    • 对象的属性是进行的 8 字节对齐,对象自己进行的是 16 字节对齐;
    • 因为内存是连续的,通过 16 字节对齐规避风险和容错,防止访问溢出;
    • 同时,也提高了寻址访问效率,也就是空间换时间;
  • calloc内存计算的示意流程如下:

iOS之深入解析内存对齐的底层原理相关推荐

  1. iOS之深入解析对象isa的底层原理

    对象本质 一.NSObject 本质 OC代码的底层实现实质是 C/C++代码 ,继而编译成汇编代码,最终变成机器语言. ① clang C/C++ 编译器 Clang 是⼀个 C 语⾔.C++.Ob ...

  2. iOS之深入解析weak关键字的底层原理

    一.weak 关键字 在 iOS 开发过程中,会经常使用到一个修饰词 weak,使用场景大家都比较清晰,避免出现对象之间的强强引用而造成对象不能被正常释放最终导致内存泄露的问题. weak 关键字的作 ...

  3. Swift之深入解析内存管理的底层原理

    一.Swift 内存管理 ① ARC 跟 OC 一样,Swift 也是采用基于引用计数的 ARC 内存管理方案(针对堆空间): Swift 的 ARC 中有三种引用: 强引用(strong refer ...

  4. iOS之深入解析类Class的底层原理

    内存偏移 定义一个数组并打印数组中的元素地址: int a[4] = {1,2,3,4};int *b = a;NSLog(@"%p - %p -

  5. iOS之深入解析分类Category的底层原理

    一.Category 简介 Objective-C 中的 Category 是对装饰模式的一种具体实现.它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法. 分类 Category 可以 ...

  6. iOS之深入解析通知NSNotification的底层原理

    一.概念 ① NSNotification NSNotification 用于描述通知的类,一个 NSNotification 对象就包含了一条通知的信息,NSNotification 对象是不可变的 ...

  7. iOS之深入解析数组遍历的底层原理和性能分析

    一.OC 数组的类体系 当我们创建一个 NSArray 对象时,实际上得到的是 NSArray 的子类 __NSArrayI 对象.同样的,创建 NSMutableArray 对象,得到的同样是其子类 ...

  8. iOS之深入解析缓存方法cache_t底层原理

    一.cache_t 原理 Class 内部中有个方法 缓存 cache_t ,用 散列表 来缓存调用过的方法,可以提高访问方法的速度. struct cache_t {#if CACHE_MASK_S ...

  9. iOS之深入解析内存管理NSTimer的强引用问题

    一.强引用问题分析 现在有两个控制器 A.B,从 A push 到 B 控制器,在 B 控制器中有如下代码: self.timer = [NSTimer timerWithTimeInterval:1 ...

最新文章

  1. 离线轻量级大数据平台Spark之MLib机器学习库概念学习
  2. python 累加器_Python编程第5课:累加器,变量与赋值进阶练习
  3. 【Python】pip工具使用知识,模型保存pickle,PDF与docx相互转换处理
  4. mysql慢查询检查流程_简单谈谈MySQL优化利器-慢查询
  5. linux mysql 停止,linux 里 重启 和停止 mysql的原理
  6. 论文浅尝 | 六篇2020年知识图谱预训练论文综述
  7. 【ACL2021】BERT也能做生成?利用多个BERT模型分离对话生成和对话理解
  8. Python3实现简易的学生选课系统
  9. (2)安装宝塔与docker及docker镜像下载加速
  10. 对称密钥和非对称密钥有什么区别,区别在哪里
  11. Map集合简单应用的例子(世界杯)
  12. JSLint中常见报错提示
  13. Swift零基础学习之用TableView做个景点App
  14. 2022-2027年中国卫星遥感市场竞争态势及行业投资前景预测报告
  15. JAVA鞍山丘比特房屋租赁管理系统计算机毕业设计Mybatis+系统+数据库+调试部署
  16. 怎么批量提取网站中的内容-免费网页数据提取软件
  17. tomcat更改默认端口号
  18. 自己写操作系统学习总结
  19. 经验分享 | VulnHub靶场学习:HA-Avengers-Arsenal
  20. 跑赢新趋势 | 未来3-5年,运维人的机会在哪里?

热门文章

  1. [转帖]Linux修改时区
  2. day4 Python的selenium库
  3. Android开源工具库
  4. Android学习笔记36:使用SQLite方式存储数据
  5. noip2008普及组4题题解-rLq
  6. 第12章 与Spring集成
  7. 启动多线程的两种情况比较
  8. 思科pix防火墙配置实例大全
  9. 宝塔网设置伪静态进行隐藏php后缀名,nextcloud宝塔面板nginx伪静态-去除index.php
  10. 笔记本电脑锁_小雷问答丨3000-3500 价格的笔记本电脑怎么选?