iOS ------ tagged Pointer 内存对齐

打印 上一主题 下一主题

主题 833|帖子 833|积分 2499

一,tagged Pointer

为了节省内存和进步执行效率,苹果在64bit程序中引入了Tagged Pointer计数,用于优化NSNumber, NSDate, NSString等小对象的存储。一个指针或地点区域,除了放对象地点之外,也可以放其他额外的信息,并将此中的一些bit位作为tag标记区分,这就叫做Tagged Pointer。
从占用内存来看

指针类型的巨细通常也是与 CPU 位数相关,一个指针所在 32 bit 下占用 4 个字节,在 64 bit 下占用 8 个字节。
NSNumber等对象的指针中存储的数据酿成了Tag+Data情势(Tag为特殊标记,用于区分NSNumber、NSDate、NSString等对象类型;Data为对象的值)。如许利用一个NSNumber对象只需要 8 个字节指针内存。当指针的 8 个字节不够存储数据时,才会在将对象存储在堆上。
在 64 bit 下,假如没有利用Tagged Pointer的话,为了利用一个NSNumber对象就需要 8 个字节指针内存和 32 个字节对象内存。
  1.     NSInteger i = 0xFFFFFFFFFFFFFF;
  2.     NSNumber *number = [NSNumber numberWithInteger:i];
  3.     NSLog(@"%zd", malloc_size((__bridge const void *)(number))); // 32
  4.     NSLog(@"%zd", sizeof(number)); // 8
复制代码
利用了Tagged Pointer且指针的8歌字节够存储数据,NSNumber对象的值直接存储在了指针上,不会在堆上申请内存。则利用一个NSNumber对象只需要指针的 8 个字节内存就够了,大大的节省了内存占用。
  1. NSInteger i = 1;
  2. NSNumber *number = [NSNumber numberWithInteger:i];
  3. NSLog(@"%zd", malloc_size((__bridge const void *)(number))); // 0
  4. NSLog(@"%zd", sizeof(number)); // 8
复制代码
从效率上来看

为了利用一个NSNumber对象,需要在堆上为其分配内存,还要维护它的引用计数,管理它的生命周期,影响执行的效率
NSNumber

  1. int main(int argc, const char * argv[]) {
  2.     @autoreleasepool {
  3.         NSNumber *number1 = @1;
  4.         NSNumber *number2 = @2;
  5.         NSNumber *number3 = @3;
  6.         NSNumber *number4 = @(0xFFFFFFFFFFFFFFFF);
  7.    
  8.         NSLog(@"%p %p %p %p", number1, number2, number3, number4);
  9.     }
  10.     return 0;
  11. }
  12. // 关闭 Tagged Pointer 数据混淆后:0x127 0x227 0x327 0x600003a090e0
  13. // 关闭 Tagged Pointer 数据混淆前:0xaca2838a63a4fb34 0xaca2838a63a4fb04 0xaca2838a63a4fb14 0x600003a090e0
复制代码
number1~number3指针为Tagged Pointer类型,可以看到对象的值都存储在了指针中,对应0x1、0x2、0x3。而number4由于数据过大,指针的8个字节不够存储,以是在堆中分配了内存。
0x127 中的 2 和 7 表示什么?
我们先来看这个7,0x127为十六进制表示,7的二进制为0111。最后一位1是Tagged Pointer标识位,代表这个指针是Tagged Pointer。前面的011是类标识位,对应十进制为3,表示NSNumber类。
可以在Runtime源码objc4中检察NSNumber、NSDate、NSString等类的标识位
  1. // objc-internal.h
  2. {
  3.     OBJC_TAG_NSAtom            = 0,
  4.     OBJC_TAG_1                 = 1,
  5.     OBJC_TAG_NSString          = 2,
  6.     OBJC_TAG_NSNumber          = 3,
  7.     OBJC_TAG_NSIndexPath       = 4,
  8.     OBJC_TAG_NSManagedObjectID = 5,
  9.     OBJC_TAG_NSDate            = 6,
  10.     ......
  11. }
复制代码
0x127 中的 2(即倒数第二位)又代表什么呢?
倒数第二位用来表示数据类型。
Tagged Pointer倒数第二位对应数据类型:
   0: char
1: short
2: int
3: long
4: float
5: double
  

NSString

  1. int main(int argc, const char * argv[]) {
  2.     @autoreleasepool {
  3.         NSString *a = @"a";
  4.         NSMutableString *b = [a mutableCopy];
  5.         NSString *c = [a copy];
  6.         NSString *d = [[a mutableCopy] copy];
  7.         NSString *e = [NSString stringWithString:a];
  8.         NSString *f = [NSString stringWithFormat:@"f"];
  9.         NSString *string1 = [NSString stringWithFormat:@"abcdefg"];
  10.         NSString *string2 = [NSString stringWithFormat:@"abcdefghi"];
  11.         NSString *string3 = [NSString stringWithFormat:@"abcdefghij"];
  12.     }
  13.     return 0;
  14. }
  15. a: 0x100002038, __NSCFConstantString, 18446744073709551615
  16. b: 0x10071f3c0, __NSCFString, 1
  17. c: 0x100002038, __NSCFConstantString, 18446744073709551615
  18. d: 0x6115, NSTaggedPointerString, 18446744073709551615
  19. e: 0x100002038, __NSCFConstantString, 18446744073709551615
  20. f: 0x6615, NSTaggedPointerString, 18446744073709551615
  21. string1: 0x6766656463626175, NSTaggedPointerString, 18446744073709551615
  22. string2: 0x880e28045a54195, NSTaggedPointerString, 18446744073709551615
  23. string3: 0x10071f6d0, __NSCFString, 1 */
复制代码
为Tagged Pointer的有d、f、string1、string2指针。它们的指针值分别为0x6115、0x6615 、0x6766656463626175、0x880e28045a54195。
此中0x61、0x66、0x67666564636261分别对应字符串的 ASCII 码。
最后一位5的二进制为0101,最后一位1是代表这个指针是Tagged Pointer,010对应十进制为2,表示NSString类。
倒数第二位1、1、7、9代表字符串长度

NSString的类型NSString类型
注意: MacOS与iOS平台下的Tagged Pointer有差别:
   MacOS下接纳 LSB(Least Significant Bit,即最低有效位)为Tagged Pointer标识位
iOS下则接纳MSB(Most Significant Bit,即最高有效位)为Tagged Pointer标识位。
  下图是iOS下NSNumber的Tagged Pointer位视图: Tagged Pointer 位视图

下图是iOS下NSString的Tagged Pointer位视图:

相关标题

执行以下两段代码,有什么区别?
  1.     dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
  2.     for (int i = 0; i < 1000; i++) {
  3.         dispatch_async(queue, ^{
  4.             self.name = [NSString stringWithFormat:@"abcdefghij"];
  5.         });
  6.     }
复制代码
  1.     dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
  2.     for (int i = 0; i < 1000; i++) {
  3.         dispatch_async(queue, ^{
  4.             self.name = [NSString stringWithFormat:@"abcdefghi"];
  5.         });
  6.     }
复制代码
第一段代码会报错
第一段代码中self.name为__NSCFString类型,而第二段代码中为NSTaggedPointerString类型。__NSCFString存储在堆上,它是个正常对象,需要维护引用计数的。self.name通过setter方法为其赋值。而setter方法的实现如下:
  1. - (void)setName:(NSString *)name {
  2.     if(_name != name) {
  3.         [_name release];
  4.         _name = [name retain]; // or [name copy]
  5.     }
  6. }
复制代码
我们异步并发执行setter方法,大概就会有多条线程同时执行[_name release],连续release两次就会造成对象的过度开释,导致Crash。
解决办法:


  • 利用atomic属性关键字。
  • 加锁
而第二段代码中的NSString为NSTaggedPointerString类型,在objc_release函数中会判断指针是不是TaggedPointer类型,是的话就不对对象举行release操作,也就避免了因过度开释对象而导致的Crash,由于根本就没执行开释操作。
  1. objc_release(id obj)
  2. {
  3.     if (!obj) return;
  4.     if (obj->isTaggedPointer()) return;
  5.     return obj->release();
  6. }
复制代码
二,内存对齐

在iO64位体系中,接纳8字节对齐(盘算属性内存空间巨细总和),最小内存巨细为16个字节,现实分配空间是16字节对齐。
在盘算机中,内存巨细的基本单元是字节,理论上可以在任意地点在访问某种基本数据类型。而盘算机并非按早字节巨细读写内存,而是以2,4,8的字节块来读写内存。因此,编译器会对基本数据类型的正当地点做出一些限定,地点必须是2,4,8的倍数。那么就要求各种数据类型按早一定的规则在空间上排列,这就是内存对齐
对象的属性内存布局遵照下面规则:


  • 布局体变量的首地点是其最长基本类型成员的整数倍
  • 布局体的总巨细为布局体最大基本类型成员变量的整数倍
  • 布局体每个成员相对于布局体首地点的偏移量(offset)都是成员巨细的整数倍,如不满足,对前一个成员填充字节以满足
  • 假如一个布局体内部成员变量包括其他布局体成员,则布局体成员要从其内部成员最大元素巨细的整数倍地点开始储存
  • 布局体中的成员变量都是分配在连续的内存空间中
  • 布局体成员顺序差别,会导致所占内存空间不一样;对象经过编译器优化,就不会有这个问题
实例:
  1. #import <Foundation/Foundation.h>
  2. #import <objc/runtime.h>
  3. #import <malloc/malloc.h>
  4. #import "Person.h"
  5. int main(int argc, const char * argv[]) {
  6.     @autoreleasepool {
  7.         //person 有name,age属性
  8.         //如果对象创建了没去赋值属性,它会是内存假地址
  9.         Person* person = [[Person alloc] init];
  10.         person.name = @"111";
  11.         person.age = 20;
  12.         //class_getInstanceSize依赖于<ojc/runtime.h>返回创建一个实例对象的内存大小就是获取对象的全部属性的大小
  13.         NSLog(@"class_getInstanceSize = %zd", class_getInstanceSize([Person class]));//输出24
  14.         //malloc_size依赖于<malloc/malloc.h>返回给系统分配给对象的内存大小,而且最小是16字节。就是获取对象的全部属性的大小总和,然后按8位对齐获得,不足8位补齐8位。        
  15.         NSLog(@"malloc_size = %zd", malloc_size((__bridge  const void*)person));//输出32
  16.         //最后 sizeOf 得到的内存大小都是8个字节, 是因为 sizeOf获取的是类型所分配内存,所传参数为指针类型,所以最后得到的都是8
  17.         NSLog(@"sizeof  = %zd", sizeof(person));//输出8
  18.         }
  19.     return 0;
  20. }
复制代码


  • class_getInstance 获取实例对象在内存对齐的情况下,所占巨细
  • malloc_size 获取的是现实体系所分配的内存巨细
  • sizeOf 获取类型所占字节巨细,假如传的是对象,永远都是8;
内存对齐的原因


  • 性能上的提升
    从内存的占用的角度来讲,对齐后比未对齐有些情况反而增加了内存分配的开支。数据布局(尤其是栈)应该静大概在自然边界对齐,为了访问为对齐的内存,处理器会举行两次的内存访问;而对齐的内存访问仅需要一次的访问,最重要进步了内存体系的性能。
  • 跨平台
    某些硬性的平台不能访问任意地点上的任意数据的,只能 处理特定类型的数据,否则会导致硬件基基层的错误。
注意:
假如给类添加方法,类实例对象内存巨细是不会变化的,为什么那?
创建对象的时间并不会给对象的方法分配内存,只会给属性,成员变量分配内存。一个类大概创建多个实例,每个实例的方法都一样,没有差别性,所有对象共用这块存储方法的内存,现实上方法都存储在类实例内里了,一个类只有一个类实例,由体系创建。这么设计的利益就是节省空间,加快初始化速度等

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

羊蹓狼

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表