iOS底层原理02-怎么就内存对齐了呢
怎么查看内存
- 通过sizeof可以获取基本数据类型的内存占用,一般用于查看栈空间中基本数据类型内存情况:
// 1. 基本数据:
NSLog(@"BOOL:%lu",sizeof(BOOL)); // BOOL:1
NSLog(@"short:%lu",sizeof(short)); // short:2
NSLog(@"int:%lu",sizeof(int)); // int:4
NSLog(@"long:%lu",sizeof(long)); // long:8
NSLog(@"float:%lu",sizeof(float)); // float:4
NSLog(@"double:%lu",sizeof(double)); // double:8
// 2.结构体数据:
struct TheStructOne{
int one;
}TheStructOne;
struct TheStructTwo{
int one;
int two;
}TheStructTwo;
struct TheStructThree{
int one;
int two;
int three;
}TheStructThree;
NSLog(@"TheStructOne:%lu",sizeof(TheStructOne)); // TheStructOne:4
NSLog(@"TheStructTwo:%lu",sizeof(TheStructTwo)); // TheStructTwo:8
NSLog(@"TheStructThree:%lu",sizeof(TheStructThree)); // TheStructThree:12
- 通过class_getInstanceSize方法可以对象在堆空间中实际占用的内存情况,通过malloc_size可以获取实际为对象开辟的内存空间情况,这两者都只对对象有效,一般用于查看堆空间中对象内存情况:
// 0. 定义一个函数打印内存占用情况
void printMemory(NSObject *objc)
{
NSLog(@"类型:%@->类型占用,实际占用,实际分配:%lu %lu %lu",objc.class,sizeof(objc),class_getInstanceSize([objc class]),malloc_size((__bridge const void*)(objc)));
}
// 1.基类内存情况
printMemory([NSObject alloc]); // 类型:NSObject->类型占用,实际占用,实际分配:8 8 16
// 2.自定义类内存情况
@interface TheObjectOne : NSObject
{
int one; // 4
}
@end
@interface TheObjectTwo : NSObject
{
int one; // 4
int two; // 4
}
@end
@interface TheObjectThree : NSObject
{
int one; // 4
int two; // 4
int three; // 4
}
@end
printMemory([TheObjectOne alloc]); // 类型:TheObjectOne->类型占用,实际占用,实际分配:8 16 16
printMemory([TheObjectTwo alloc]); // 类型:TheObjectTwo->类型占用,实际占用,实际分配:8 16 16
printMemory([TheObjectThree alloc]); // 类型:TheObjectThree->类型占用,实际占用,实际分配:8 24 32
printMemory([TheObjectFour alloc]); // 类型:TheObjectFour->类型占用,实际占用,实际分配:8 24 32
printMemory([TheObjectFiv alloc]); // 类型:TheObjectFiv->类型占用,实际占用,实际分配:8 32 32
printMemory([TheObjectSix alloc]); // 类型:TheObjectSix->类型占用,实际占用,实际分配:8 32 32
printMemory([TheObjectSeven alloc]); // 类型:TheObjectSeven->类型占用,实际占用,实际分配:8 40 48
printMemory([TheObjectEight alloc]); // 类型:TheObjectEight->类型占用,实际占用,实际分配:8 40 48
printMemory([TheObjectNine alloc]); // 类型:TheObjectNine->类型占用,实际占用,实际分配:8 48 48
printMemory([TheObjectTen alloc]); // 类型:TheObjectTen->类型占用,实际占用,实际分配:8 48 48
内存怎么是这样的
好像有的结论
- 结构体实际占用空间大小是各基础数据类型的总大小之和。
- 对象类型恒定占用为8,实际占用空间最低8,实际分配最低16。而实际占用空间以8为跨度增长,实际分配空间最低以16为跨度增长。
那么是不是呢?
结构体占用大小
我们看看结构体,将结构体数据类型换成其它基础类型:
struct TheStructCharOne{
char one;
}TheStructCharOne;
struct TheStructCharTwo{
char one;
int two;
}TheStructCharTwo;
struct TheStructCharThree{
char one;
int two;
int three;
};
NSLog(@"%lu",sizeof(TheStructCharOne)); // 1
NSLog(@"%lu",sizeof(TheStructCharTwo)); // 8
NSLog(@"%lu",sizeof(TheStructCharThree)); // 12
和我们想的不一样,大小从1突然就到8了,有点像类实例分配时候增长的跨度。
类占用大小
首先通过命令 clang -rewrite-objc main.m -o main.cpp
将main.m编译为C++,为了方便查看 我们将编译后的main.cpp拖入到xcode工程中,然后为了解除编译报错,从编译源码中移除:
我们可以看到编译后的TheObjectThree变成了:
struct NSObject_IMPL {
Class isa; // 8
};
struct TheObjectThree_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int one; // 4
int two; // 4
int three; // 4
};
结构体包含结构体等价于把子结构体的所有成员全部放入父结构体中,所以相当于:
struct TheObjectThree_IMPL {
struct NSObject_IMPL {
Class isa; // 8
};
int one; // 4
int two; // 4
int three; // 4
};
可以看出来类的本质是结构体,只不过自定义类相比普通结构体都会多出一个8字节的isa成员。所以搞清楚了结构体为什么实际分配内存空间比成员结构实际占用空间计算出来的大,就能搞清楚类的相同情况。
内存对齐是什么
我们先想一想计算机是怎么读取内存的。不同的处理器根据处理能力不同会一次性读取固定位数的内存块,这个我们称之为内存存取粒度。
我们知道了内存是按块来读取的,现在假设一个结构的大小刚好在一个内存块的大小范围内,理想情况下只需要一次就能读取成功,但如果它的起始位置在上一个内存块,结束在另一个块,那么这个CPU只能读取两次,带来了存取效率上的损失,而且中间还会做剔除和合并。所以为了提高效率及方便读取,我们需要将这个内存进行对齐,使其能在最小读取次数内进行访问。
对于某些架构的CPU甚至会发生变量不对齐就报错,这种情况下只能保证能存对齐。
对齐规则
内存对齐原则
- 数据成员对齐原则: 结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小.
- 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.
- 结构体的总大小,也就是sizeof的结果,必须是其内部最大 成员的整数倍,不足的要补⻬.
总之,内存对齐原则就是:min(m,n) //m为开始的位置,n为所占位数。当m是n的整数倍时,条件满足;否则m位空余,m+1,继续min算法:
如MyStruct1实际占用计算过程:
a:从0开始,此时min(0,1),即[0]存储a
b:从1开始,此时min(1,8),1%8!=0,继续往后移动,直到min(8,8),即[8-15]存储b
c:从16开始,此时min(16,4),16%4=0,即[16-19]存储c
d:从20开始,此时min(20, 2),20%2=0,即[20-21]存储d
通过这一步骤,我们可以做一些优化重排的工作:
struct Optimize1 {
double a; // 8
int b; // 4
bool c; // 1
}Optimize1;
/*
min(0,8)=a=>[0-7],min(8,4)=b=>[8-11],min(12, 1)=c=>[12];
实际大小为13bytes,最大变量的a字节数为8,最小的满足8的整数倍的是16,Optimize1分配内存为16bytes.
*/
struct Optimize2 {
int d; // 4
double e; // 8
bool f; // 1
}Optimize2;
/*
min(0,4)=d=>[0-3],min(4,8)=e=>[8-15],min(16, 1)=f=>[16];
实际大小为17bytes,最大变量的a字节数为8,最小的满足8的整数倍的是24,Optimize2分配内存为24bytes.
*/
这里我们想到了类的结构体嵌套,试着根据规则计算结构体嵌套:
struct Optimize3 {
double a; // 8
int b; // 4
bool c; // 1
struct Optimize2 opt2; // 24
}Optimize3; // 结构体大小:40,结构体成员大小opt2:24
/*
min(0,8)=a=>[0-7],min(8,4)=b=>[8-11],min(12, 1)=c=>[12];
min(16,24)=opt2=>[16,40]
*/
struct Optimize4 {
struct Optimize2 opt2; // 24
double a; // 8
int b; // 4
bool c; // 1
}Optimize4; // 结构体大小:40,结构体成员大小opt2:24
/*
min(0,24)=opt2=>[0,23];
min(24,8)=a=>[24-31],min(32,4)=b=>[32-35],min(36, 1)=c=>[36]; // 40
*/
struct Optimize5 {
double a; // 8
int b; // 4
bool c; // 1
int d; // 4
double e; // 8
bool f; // 1
}Optimize5; // 结构体大小:40
/*
min(0,8)=a=>[0-7],min(8,4)=b=>[8-11],min(12, 1)=c=>[12];
min(13,4)=d=>[16-19],min(20,8)=e=>[24-31],min(32, 1)=f=>[32]; // 40
*/
可以看出来结构体的嵌套等同于将从子结构体中将成员变量直接放到父成员变量中,所以TheObjectOne_IMPL结构体等价与:
struct TheObjectOne_IMPL {
Class isa; // 8
int one; // 4
}; // 16
根据以上规则,NSObject类对应的NSObject_IMPL结构体对应的类型占用/实际占用/实际分配应该为8/8/8,为何NSObject最后打印出来的结果是8/16/16呢?通过OC源码,我们窥探一下:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
# define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
可以看到,class_getInstanceSize就是获取实例对象中成员变量的内存大小。 而(x + WORD_MASK) & ~WORD_MASK
相当于(x+7) & ~7
:
8的二进制 0000 1000 后三位都是0
7的二进制 0000 0111 后三位都是1
~7的后三位都是000,经过&运算后三位一定是000,最后的结果必定是8的倍数.
(x+7)的含义是任意一个数给你最大的可能性升阶(8的n阶乘).
如1+7=8,8的一阶.11+7=18.就是8的2阶2*8=16.相比16相差2,所以后三位的就不管了直接&运算就抹零了.
所以class_getInstanceSize最小返回为16.
我想影响内存对齐
这里有两个编译指令:
#pragma pack(n)
n就是你要指定的“对齐系数”,一次性可以从内存中读/写n个字节.n=1,2,4,8,16.
#pragma pack()
取消自定义字节对齐.
__attribute__((aligned (n)))
让所作用的结构成员对齐在n字节自然边界上.
__attribute__((packed))
取消优化对齐,按照实际占用字节数对齐.
内存对齐的"对齐数"取决于"对齐系数"和"成员的字节数"两者之中的较小值。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
智慧数字“冰城”,“硬”核转型加速 | 华为中国生态之行2020·哈尔滨数字产业峰会成功举办
9月11日,“华为中国生态之行2020·哈尔滨数字产业峰会”在哈尔滨市成功举办,来自哈尔滨各界的城市管理者、企业高层、专家学者、客户伙伴共聚一堂,围绕“行业数字化转型加速时”的主题,共话冰城产业结构升级以及行业数字化转型,为哈尔滨数字经济发展注入新动能。 “数字龙江”建设,冰城硬核加速 数字经济是中国经济发展的新动能、新引擎。为落实《国家信息化发展战略纲要》、《数字经济发展战略纲要》,2019年黑龙江省人民政府发布了《“数字龙江”发展规划(2019-2025年》,旨在为高质量推进“数字龙江”建设提供行动指南。黑龙江省工业和信息化厅一级巡视员方安儒在致辞中表示,近年来华为围绕龙江经济社会发展大局,在“数字龙江”诸多领域贡献了华为方案,在推动产业数字化、数字产业化进程中发挥了重要作用。希望未来继续携手华为不断开创龙江数字经济发展新局面。 黑龙江省工业和信息化厅一级巡视员 方安儒 华为中国地区部副总裁强华在致辞中表示,“新基建”的核心是建设数字基础设施,即依靠新联接、新计算,加速行业的数字化转型进程,创造更多的“中国式新速度”。新计算是行业数字化转型的发动机。新联接带来的数据指数级增长,需要...
-
下一篇
每日一博 | 导致 MySQL 索引失效的几种常见写法
最近一直忙着处理原来老项目遗留的一些SQL优化问题,由于当初表的设计以及字段设计的问题,随着业务的增长,出现了大量的慢SQL,导致MySQL的CPU资源飙升,基于此,给大家简单分享下这些比较使用的易于学习和使用的经验。 这次的话简单说下如何防止你的索引失效。 再说之前我先根据我最近的经验说下我对索引的看法,我觉得并不是所以的表都需要去建立索引,对于一些业务数据,可能量比较大了,查询数据已经有了一点压力,那么最简单、快速的办法就是建立合适的索引,但是有些业务可能表里就没多少数据,或者表的使用频率非常不高的情况下是没必要必须要去做索引的。就像我们有些表,2年了可能就10来条数据,有索引和没索引性能方面差不多多少。 索引只是我们优化业务的一种方式,千万为了为了建索引而去建索引。 下面是我此次测试使用的一张表结构以及一些测试数据 CREATETABLE`user`(`id`int(5)unsignedNOTNULLAUTO_INCREMENT,`create_time`datetimeNOTNULL,`name`varchar(5)NOTNULL,`age`tinyint(2)unsigned...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS8编译安装MySQL8.0.19
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2全家桶,快速入门学习开发网站教程
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Docker使用Oracle官方镜像安装(12C,18C,19C)