iOS——Tagged Pointer

Tagged Pointer是什么

标记指针(Tagged Pointer)是一种优化技术,用于在不分配额外内存的情况下存储小的对象或数字值。在这种技术中,指针的最低有效位(LSB)用于存储特殊标记,而不是指向分配的内存地址。
传统上,Objective-C 对象都是通过指针引用的,指针指向一个存储在堆内存中的对象实例。然而,对于一些小的对象,如NSNumber、NSDate等,它们只包含了一些简单的数据,没有必要为它们分配额外的内存空间。因此,为了节省内存和提高性能,Objective-C 引入了标记指针。
标记指针通过修改指针的最低有效位来存储一些简单的值,如整数、浮点数、布尔值等。这样,当对象的大小小于一个指针的大小时,可以直接将数据存储在指针中,而不需要额外的内存分配。这种方式避免了内存分配和释放的开销,提高了程序的性能和内存利用率。

  • 苹果对于Tagged Pointer特点的介绍:

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

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

为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer对象。由于NSNumber、NSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿。所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。
在这里插入图片描述

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

Tagged Pointer的使用与示例

示例:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableString *str = [NSMutableString stringWithString:@"abcde"];
        for (int i = 0; i < 10; i++) {
            [str appendFormat:@"f"];
            NSString *newStr = [str copy];
            NSLog(@"%@ %@",newStr ,[newStr class]);
        }
    }
    return 0;
}

输出结果:
在这里插入图片描述

当字符串的长度为10个以内时,字符串的类型都是NSTaggedPointerString类型,当超过10个时,字符串的类型才是__NSCFString

Tagged Pointe相关的一些注意

我们使用刚刚的例子:

NSMutableString *str = [NSMutableString stringWithString:@"abcde"];
NSString *newStr = [str copy];
NSLog(@"%@ %p", [newStr class], newStr);

得到的结果是:
在这里插入图片描述

可见这里的newStr使用了Tagged Pointer优化。
但是我们再更改一下代码:

NSMutableString *str = [NSMutableString stringWithString:@"abcde"];
NSString *newStr = [str mutableCopy];
NSLog(@"%@ %p", [newStr class], newStr);

结果又是:
在这里插入图片描述

这是因为:Tagged Pointer主要用于优化小的、不可变的对象
对于可变对象,比如NSMutableString,其值在创建后可能会发生改变,所以不能使用Tagged Pointer。因为如果一个对象的值可以改变,那么将其值直接存储在指针的地址中就不可行,因为这样做的话,每次值改变都需要重新计算指针的地址,这将非常低效。
相反,对于不可变的对象,比如NSString,其值在创建后就不会再改变,所以可以使用Tagged Pointer技术,将它的值直接存储在指针的地址中,这样可以节省内存,提高效率。

那么我们在看下面这段代码:

NSString *str = @"abcde";
NSLog(@%@ %p", [str class], str);

结果:
在这里插入图片描述

现在str是一个不可变的字符串,而且只有五个字符,为什么它的class不是NSTaggedPointerString而是__NSCFConstantString呢?
这是因为__NSCFConstantString是编译时确定的字符串,它们的值和内存地址在程序运行期间是不会改变的。而str在编译时就已经确定了,它的值和内存地址在程序运行期间是不会改变的,所以其类型是__NSCFConstantString,而不是NSTaggedPointerString。

Tagged Pointer 结构

在Tagged Pointer系统中,我们可以将一个小的数据直接存储在指针的地址中。当我们访问这个指针时,系统会检查这个地址的最高位(在iOS中是最高位,而在macOS中是最低位)。如果最高位是1,那么系统就知道这是一个Tagged Pointer,接下来的几位则用来表示这个数据的类型,剩下的位则用来存储实际的数据。

Tagged Pointer的地址是经过编码的,其结构如下:

  • Tagged Pointer 标记:这是用来标记该对象是否为Tagged Pointer对象的标志位。在x86架构中,这是最后一位;在arm64架构中,这是最高位。1表示是Tagged Pointer对象,0表示是普通对象。
  • Tag:这是对象类型的标记。在x86架构中,它占据13位;在arm64架构中,它占据2位。其中,值为7表示有扩展信息。
  • Extended:这部分用于扩展更多类型。在x86架构中,它占据4位;在arm64架构中,它占据5位。
  • payload:这是有效负载,用于存储真正的数据(除了标记位、tag以及extended)。但是为了安全,苹果对其进行了编码。

比如这个NSTaggedPointerString的地址:
在这里插入图片描述

苹果为了安全对其做了编码,runtime内部实现了编码、解码方法,我们看一下:
编码:

static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
    if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
        return (void *)ptr;
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag);
    value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
    value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;
#endif
    return (void *)value;
}

isa 指针

Tagged Pointer的引入也带来了问题,即Tagged Pointer因为并不是真正的对象,而是一个伪对象,所以所有对象都有 isa 指针,而Tagged Pointer其实是没有的,因为它不是真正的对象。
但是只要避免在代码中直接访问对象的 isa 变量,即可避免这个问题。

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

64位下的isa指针优化

对于 64位设备,苹果除了引人 Tagged Pointer 来优化小的对象外,对于普通的对象,其isa指针也进行了优化和调整。在32 位环境下,对象的引用计数都保存在一个外部的表中,每一个对象的 Retain 操作,实际上包括如下 5个步骤:

  1. 获得全局的记录引用计数的hash 表。

  2. 为了线程安全,给该hash 表加锁。

  3. 查找到目标对象的引用计数值。

  4. 将该引用计数值加1,写回hash 表。

  5. 给该hash 表解锁。
    从上面的步骤我们可以看出,为了保证线程安全,对引用计数的增减操作都要先锁定这个表,这从性能上看是非常差的。
    而在 64 位环境下,isa 指针也是64 位,实际作为指针部分只用到的其中33位,剩余的 31位苹果使用了类似 Tagged Pointer 的概念,其中 19 位将保存对象的引1用计数,这样对引用计数的操作只需要修改这个指针即可。只有当引用计数超出19位,才会将引用计数保存到外部表,而这种情况是很少的,所以这样引用计数的更改效率会更高。
    与前面的5个步骤对应,在64位环境下,新的 Retain 操作包括如下 5个步骤:

  6. 检查 isa 指针上面的标记位,看引1用计数是否保存在 isa变量中,如果不是,则使用以前的步骤,否则执行第2步

  7. 检查当前对象是否正在释放,如果是,则不做任何事情。

  8. 增加该对象的引用计数,但是并不马上写回到isa 变量中。

  9. 检杳增加后的引用计数的值是否能够被 19 位表示,如果不是,则切换成以前的办法,否则执行第5步。

  10. 进行一个原子的写操作,将isa 的值写回。
    虽然步骤都是 5步,但是由于没有了全局的加锁操作,所以引用计数的更改更快了。

一个经典的面试题:

这两段代码运行分别会是什么结果:

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"addasdsaadss"];
        });
    }
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"ad"];
        });
    }

两段代码唯一的区别就是一个name属性所赋值的字符串长一些长度大于10,另一个长度小一点小于10。我们去运行它,就会发现,第一段代码程序崩溃,第二段没有崩溃。

原因就是:第一段代码并发访问了共享数据 self.name。在多线程环境下,同时对同一变量进行写操作可能引发竞争条件或数据不一致的问题。要解决它就要给它加上锁。

而第二段因为字符串短,所以被改为了Tagged Pointer对象:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值