写点什么

深入理解 Tagged Pointer

2014 年 5 月 30 日

前言

在 2013 年 9 月,苹果推出了 iPhone5s ,与此同时,iPhone5s 配备了首个采用 64 位架构的 A7 双核处理器,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念。对于 64 位程序,引入 Tagged Pointer 后,相关逻辑能减少一半的内存占用,以及 3 倍的访问速度提升,100 倍的创建、销毁速度提升。本文从Tagged Pointer试图解决的问题入手,带领读者理解Tagged Pointer的实现细节和优势,最后指出了使用时的注意事项。

问题

我们先看看原有的对象为什么会浪费内存。假设我们要存储一个 NSNumber 对象,其值是一个整数。正常情况下,如果这个整数只是一个 NSInteger 的普通变量,那么它所占用的内存是与 CPU 的位数有关,在 32 位 CPU 下占 4 个字节,在 64 位 CPU 下是占 8 个字节的。而指针类型的大小通常也是与 CPU 位数相关,一个指针所占用的内存在 32 位 CPU 下为 4 个字节,在 64 位 CPU 下也是 8 个字节。

所以一个普通的 iOS 程序,如果没有Tagged Pointer对象,从 32 位机器迁移到 64 位机器中后,虽然逻辑没有任何变化,但这种 NSNumber、NSDate 一类的对象所占用的内存会翻倍。如下图所示:

我们再来看看效率上的问题,为了存储和访问一个 NSNumber 对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。

Tagged Pointer

为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer对象。由于 NSNumber、NSDate 一类的变量本身的值需要占用的内存大小常常不需要 8 个字节,拿整数来说,4 个字节所能表示的有符号整数就可以达到 20 多亿(注:2^31=2147483648,另外 1 位作为符号位),对于绝大多数情况都是可以处理的。

所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了Tagged Pointer对象之后,64 位 CPU 下 NSNumber 的内存图变成了以下这样:

对此,我们也可以用 Xcode 做实验来验证。我们的实验代码如下:

复制代码
int main(int argc, char * argv[])
{
@autoreleasepool {
NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSNumber *numberFFFF = @(0xFFFF);
NSLog(@"number1 pointer is %p", number1);
NSLog(@"number2 pointer is %p", number2);
NSLog(@"number3 pointer is %p", number3);
NSLog(@"numberffff pointer is %p", numberFFFF);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

在该代码中,我们将几个 Number 类型的指针的值直接输出。需要注意的是,我们需要将模拟器切换成 64 位的 CPU 来测试,如下图所示:

运行之后,我们得到的结果如下,可以看到,除去最后的数字最末尾的 2 以及最开头的 0xb,其它数字刚好表示了相应 NSNumber 的值。

复制代码
number1 pointer is 0xb000000000000012
number2 pointer is 0xb000000000000022
number3 pointer is 0xb000000000000032
numberFFFF pointer is 0xb0000000000ffff2

可见,苹果确实是将值直接存储到了指针本身里面。我们还可以猜测,数字最末尾的 2 以及最开头的 0xb 是否就是苹果对于Tagged Pointer的特殊标记呢?我们尝试放一个 8 字节的长的整数到NSNumber实例中,对于这样的实例,由于Tagged Pointer无法将其按上面的压缩方式来保存,那么应该就会以普通对象的方式来保存,我们的实验代码如下:

复制代码
NSNumber *bigNumber = @(0xEFFFFFFFFFFFFFFF);
NSLog(@"bigNumber pointer is %p", bigNumber);

运行之后,结果如下,验证了我们的猜测,bigNumber的地址更像是一个普通的指针地址,和它本身的值看不出任何关系:

复制代码
bigNumber pointer is 0x10921ecc0

可见,当 8 字节可以承载用于表示的数值时,系统就会以Tagged Pointer的方式生成指针,如果 8 字节承载不了时,则又用以前的方式来生成普通的指针。关于以上关于Tag Pointer的存储细节,我们也可以在这里找到相应的讨论,但是其中关于 Tagged Pointer的实现细节与我们的实验并不相符,笔者认为可能是苹果更改了具体的实现细节,并且这并不影响Tagged Pointer我们讨论Tagged Pointer本身的优点。

特点

我们也可以在 WWDC2013 的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:

  1. Tagged Pointer专门用来存储小的对象,例如NSNumberNSDate
  2. Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。
  3. 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。

由此可见,苹果引入Tagged Pointer,不但减少了 64 位机器下程序的内存占用,还提高了运行效率。完美地解决了小内存对象在存储和访问效率上的问题。

isa 指针

Tagged Pointer的引入也带来了问题,即Tagged Pointer因为并不是真正的对象,而是一个伪对象,所以你如果完全把它当成对象来使,可能会让它露马脚。比如我在《Objective-C 对象模型及应用》一文中就写道,所有对象都有 isa 指针,而Tagged Pointer其实是没有的,因为它不是真正的对象。 因为不是真正的对象,所以如果你直接访问Tagged Pointerisa成员的话,在编译时将会有如下警告:

对于上面的写法,应该换成相应的方法调用,如 isKindOfClassobject_getClass。只要避免在代码中直接访问对象的 isa 变量,即可避免这个问题。

总结

苹果将Tagged Pointer引入,给 64 位系统带来了内存的节省和运行效率的提高。Tagged Pointer通过在其最后一个 bit 位设置一个特殊标记,用于将数据直接保存在指针本身中。因为Tagged Pointer并不是真正的对象,我们在使用时需要注意不要直接访问其 isa 变量。


感谢高佳俊对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2014 年 5 月 30 日 04:388241
用户头像

发布了 65 篇内容, 共 51.3 次阅读, 收获喜欢 13 次。

关注

评论 2 条评论

发布
用户头像
为什么我用64位真机调试number = @1; 输出的地址并不符合预期,如下:0xfdcd4666e342a258,烦请解答
2019 年 09 月 15 日 22:11
回复
之前的版本(objc4-723之前),变量的值直接存储在指针中,很容易的可以读取出来,例如0xb000000000000012 然而现在的版本中(objc4-750之后),苹果对这个指针做了一些编码处理,不能直接看出来是Tagged Pointer。我在自己的博客中有整理。https://ityongzhen.github.io/iOS%E4%B8%AD%E7%9A%84%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0.html
2019 年 12 月 02 日 18:25
回复
没有更多了
发现更多内容

因为我的一个低级错误,生产数据库崩溃了将近半个小时

鄙人薛某

Java MySQL 数据库 故障定位

架构师训练营 一致性Hash算法Java实现

Cloud.

阿里P7岗位面试,面试官问我:为什么HashMap底层树化的标准元素个数是8

鄙人薛某

Java hashmap 面试题 哈希

啃碎并发(一):Java线程总述与概念

猿灯塔

最强总结——分布式事务处理方式

小闫

分布式 分布式锁 Java 面试 分布式存储 分布式缓存

【week05作业】

chengjing

正确的做事比做正确的事更重要

魔曦

架构师 极客大学架构师训练营

这是什么神仙面试宝典?半月看完25大专题,居然斩获阿里P7offer

码哥小胖

Java spring 面试题 java面试 大厂面试

架构师训练营作业 (第五周)

王海

极客大学架构师训练营

写给大忙人看的内存管理

cxuan

后端 操作系统

让你大显身手——掌握RocketMQ与Kafka中如何实现事务

小谈

kafka RocketMQ Java 面试 JVM原理 大厂面试

一篇文章深入理解分布式锁

独钓寒江雪

redis 分布式锁

20道Redis面试题(含答案)面试官会问的我都找到了

你是人间四月天

redis Spring Cloud Java 面试 redis6.0.0 Redis项目

架构师训练营第 5 周——学习总结

在野

极客大学架构师训练营

如何通过调试学习 nginx ?

张小方

c++ nginx 高性能 后端开发 服务器端开发

「架构师训练营」第 5 周作业 - 一致性哈希算法

guoguo 👻

极客大学架构师训练营

hash一致性算法与优化

Mr.Monkey

Spring Boot 多数据源 Redis 配置

南南

redis Spring Boot Java 面试 Redis作者

面试官:反射都不会,还敢说自己会Java?

码农月半

Java Java 面试 反射 大厂面试 java反射

架构师训练营第5周-一致性hash算法总结及作业

傻傻的帅

极客大学架构师训练营

数酒瓶童谣:从99数到0

程李文华

【week05】总结

chengjing

Uniapp使用GoEasy实现websocket实时通讯

GoEasy消息推送

uni-app websocket 即时通讯

记录一次拼多多Web前端面试【一面+二面+hr面】

阿文

Spring Cloud Spring Boot Web Java 面试

面试官80%会问的分布式事务中的“最大努力通知”事务

无予且行

Java MySQL 面试 事务 java面试

深入理解ThreadLocal:拨开迷雾,探究本质

独钓寒江雪

源码分析 ThreadLocal

你那么追捧的 SpringBoot,到底替你做了什么?

爱java爱自己

spring

没有微服务项目经验,就别去面试官那里送人头了

小谈

Java 架构 面试 微服务 SpringCloud

架构师课程第五周 作业

杉松壁

超级专家术语学习机

程李文华

深入理解队列:LinkedBlockingQueue源码深度解析

独钓寒江雪

阻塞队列 LinkedBlockingQueue Queue

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

深入理解Tagged Pointer-InfoQ