iOS/OS X的Tagged Pointer

Tagged Pointer是存在于字符串、NSNumber、NSDate等重复性高且较小的对象中的一种优化方式,用于解决重复性较高的类型在64位操作系统内存的内部碎片问题。

背景

在创建讨论Tagged Pointer之前,我们先认识一下它执行以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__weak NSNumber *weakNumber;
__weak NSString *weakString;
__weak NSDate *weakDate;
int num = 123;

@autoreleasepool {
weakNumber = [NSNumber numberWithInt:num];
weakString = [NSString stringWithFormat:@"string%d", num];
weakDate = [NSDate dateWithTimeIntervalSince1970:0];
}

NSLog(@"WeakNumber is %@", weakNumber);
NSLog(@"WeakString is %@", weakString);
NSLog(@"WeakDate is %@", weakDate);

运行结果为:

1
2
3
WeakNumber is 123
WeakString is string123
WeakDate is Thu Jan 1 08:00:00 1970

这样的结果与想象中可能不同,我们设置weakNumber/weakString/weakDate变量为弱引用,而在NSNumber/NSString/NSDate创建的代码部分设置了自动释放,那么当运行到输出部分的代码时,数字和字符串变量理应已经释放掉了,那么结果自然应该输出nil,然而事实是他们并没有。为什么会这样?下面我们来介绍苹果在推出64位系统后为了节省内存和提高效率而引入的Tagged Pointer。

简介

在iPhone 5s推出后,处理器步入了64位架构时代,而我们知道,64位系统中变量所占字节也会随之变大,如32位系统中的int为4字节,而64位系统下是8字节。虽然值域也随之变大,但是很多情况下,增加的值域对于数字、日期、短字符串来说并不需要。造成的内部碎片问题如下图,大多数情况下64位系统增加了32位的内存浪费:

字符串在64位系统中的内部碎片问题

那么能否优化这一内存浪费问题呢?可以注意到,虽然我们每个数字对象都占用8字节,但大家实际能用到的数字可能仅仅几位,大数计算的情况很少。那么,以较低的内存消耗预先保存一些常见数字,而对于不常见的再动态创建8字节的变量的方案,就会更加节省内存,以及创建对象的性能消耗。

Tagged Pointer

知道了存在的问题,我们现在则介绍一下Tagged Pointer。它就是将大部分常用的字符串、数字对象、日期预先保存,而其不再按照64位系统的8字节大小,当我们创建的对象内容命中了Tagged Pointer预先存储的内容时,我们的对象指针会变成Tagged Pointer,指向指定的内存。我们可以看如下的例子:

1
2
3
4
5
6
int num = 123;
NSString *str1 = [NSString stringWithUTF8String:"String123"];
NSString *str2 = [NSString stringWithFormat:@"String%d", num];

NSLog(@"%p", str1);
NSLog(@"%p", str2);

打印的地址相同,证明两者指向了同一地址。

那么如何实现将这些对象不按照8字节存储的呢?
Tagged Pointer将一个对象按32位保存(4字节),而后对其绑定一个同样是4字节的Tag,从而可以方便定位它。这样一个Tag+对象总共为8字节,与32位系统中对象指针+对象内容大小相当。至于为什么对象要按4字节保存而不是由数据大小而定,则是为了避免产生外部碎片。内存图如下所示:

使用Tagged Pointer后内存对比

isa和引用计数

现在再谈一些与Tagged Pointer有关的问题。看了其内存图后,我们知道它不是真正意义上的OC对象,那么其isa指针也就无法正常使用了。我们可以通过object_getClass()方法来获取真实对象,该对象打印结果为NSTaggedPointerString__NSTaggedDate__NSCFNumber

同时,Tagged Pointer的引用计数是无穷大的,可以尝试如下代码打印:

1
2
// ARC模式下可以使用CFGetRetainCount函数获取引用计数
NSLog(@"Retain count: %ld", CFGetRetainCount((__bridge CFTypeRef)(str1)));

结果是无穷大。所以对于类似如下的题目:

1
2
3
4
5
6
7
NSString *str1 = [NSString stringWithUTF8String:"String123"];
[str1 retain];
NSString *str2 = str1;
[str2 release];
[str1 retain];
/* ......... */
NSLog(@"Retain count: %lu", [str1 retainCount]);

无论题目中如何引用和释放,最终,其引用计数都会因为它是Tagged Pointer而是无穷大。

常量与Tagged Pointer区别

当我们如下方式定义常量str3时:

1
2
3
4
int num = 123;
NSString *str1 = [NSString stringWithUTF8String:"String123"];
NSString *str1 = [NSString stringWithUTF8String:"String%d", num];
NSString *str2 = @"String123";

我们会发现str3和其余两个的地址并不一样,这是为什么呢?

我们要知道,常量不是Tagged Pointer,因为它是在运行时之前就导入的,存在于数据段内。而其余两个对象是在运行时生成的,所以才可以通过Tagged Pointer这一机制进行优化。

相关资料

  1. mikeash.com - Friday Q&A 2015-07-31: Tagged Pointer Strings
  2. mikeash.com - Friday Q&A 2012-07-27: Let’s Build Tagged Pointers