在Mac OS X 10.6(Snow Leopard)中开始支持64位,如今最新版本iPhone 5s也开始采用了Arm64架构。在64位化的过程中,其中一个比较关键的改进就是,Mac OS 10.7(Lion)和iOS 7的64位环境先后引入了Tagged Pointer。下面就简单地来介绍一下Tagged Pointer,在介绍Tagged Pointer之前有必要介绍一下指针地址对齐概念和64位环境的一些变化。

指针地址对齐

在32位环境下,如果要读取一个32位整数,如果这个32位整数在内存地址为0x00000002-0x00000006(仅作举例,这个地址一般是被系统保留的)的内存上,读取这个整数会消耗2个内存周期,而如果这个数在0x00000004-0x00000008的内存上只需要一个内存周期。为了加快内存的CPU访问,程序都使用了指针地址对齐概念。而在一些堆的内存分配算法中,一般都会采用更大的对齐块来加快访问和方便分配。指针地址对齐就是指在分配堆中的内存时往往采用偶数倍或以2为指数倍的内存地址作为地址边界。几乎所有系统架构,包括Mac OS和iOS,都使用了地址对齐概念。

1
2
3
4
void *a = malloc(1);
void *b = malloc(3);
NSLog(@"a: %p",a);
NSLog(@"b: %p",b);

运行这段代码后,我得到了如下结果:

1
2
a: 0x8c11e20
b: 0x8c11e30

可以看到,a和b指针的最后4位都是0,虽然a只占用1个字节,但是a和b的地址却相差16个字节。因为iOS中是以16个字节为内存分配边界的,或者说iOS的指针地址对齐是以16个字节为对齐边界的。进一步说,iOS中分配的内存地址最后4位永远都是0。

64位地址

在不久前发布iPhone5s中采用了Arm64的CPU,同时也支持了64位的App。64位App中指针大小也扩大到64位,就是理论上可以支持最大2^64字节(达千万T字节)的内存地址空间。而对于大多数应用来说,这么大的地址空间完全是浪费的。也就是说64位环境下,内存地址的中有很多位都是0。

Tagged Pointer

由于指针地址对齐概念和64位超大地址的出现,指针地址仅仅作为内存的地址是比较浪费的,我们可以在指针地址中保存或附加更多的信息。这就引入了Tagged Pointer概念。Tagged Pointer是指那些指针中包含特殊属性或信息的指针。其中指针对齐概念可以让我们来标识一个指针是否是Tagged Pointer以及相关类型,64位的地址指针又为我们提供保存额外信息的足够空间。如今,iOS 7的64位环境和Mac OS 10.7(Lion)中开始引入了Tagged Pointer。

NSNumber的优化

Tagged Pointer一个比较典型的应用就是NSNumber,在64位环境下,对于一般的数字,NSNumber不用再分配内存了。我们看看NSNumber是如何运用Tagged Pointer的:

1
2
3
4
5
6
NSNumber *number3 = @3;
NSNumber *number4 = @4;
NSNumber *number9 = @9;
NSLog(@"number3 pointer is %p", number3);
NSLog(@"number4 pointer is %p", number4);
NSLog(@"number9 pointer is %p", number9);

在64位模拟器中运行后,我得到了如下结果:

1
2
3
number3 pointer is 0xb000000000000032
number4 pointer is 0xb000000000000042
number9 pointer is 0xb000000000000092

可以看出number3number4number9的值前4位都是0xb,后4位都是0x2(指针的Tag),中间就是实际的取值,因此,这些NSNumber已经不需要再分配内存(指堆中内存)了,直接可以把实际的值保存到指针中,而无需再去访问堆中的数据。这无疑提高的内存访问速度和整体运算速度。

也就是说Tagged Pointer本身就可以表示一个NSNumber了,在64位环境下运行这段代码:

1
NSLog(@"0xb000000000000052's class is %@",[(NSNumber*)0xb000000000000052 class]);

会输出下面结果:

1
0xb000000000000052's class is __NSCFNumber

那么如果一个数超过了Tagged Pointer所能表示的范围,系统会怎么处理?看看这段代码:

1
2
NSNumber *numberBig = @(0x1234567890ABCDEF);
NSLog(@"numberBig pointer is %p", numberBig);

在64位模拟器中运行后,我得到了如下结果:

1
numberBig pointer is 0x1094026a0

可以看出numberBig指针最后4位都是0,应该是分配在堆中的对象。因此,如果NSNumber超出了Tagged Pointer所能表示的范围,系统会自动采用分配成对象,可以根据指针的最后4位是否为0来区分。

isa指针优化

查看NSObject类的头文件,你会发现这段定义:

1
2
3
@interface NSObject <NSObject> {
Class isa;
}

所有类都继承自NSObject,因此每个对象都有一个isa[^1]指针指向它所属的类。在《ARM64 and You》文章中指出:

在32位环境下,对象的引用计数都保存在一个外部的表中,而对引用计数的增减操作都要先锁定这个表,操作完成后才解锁。这个效率是非常慢的。

而在64位环境下,isa也是64位,实际作为指针部分只用到的其中33位,剩余的部分会运用到Tagged Pointer的概念,其中19位将保存对象的引用计数,这样对引用计数的操作只需要原子的修改这个指针即可,如果引用计数超出19位,才会将引用计数保存到外部表,而这种情况往往是很少的,因此效率将会大大提高。

[^1]: isa是“is a”,比如“Apple is a company”,表示一种从属关系。

参考文献