Mobile Development 22 min read

Understanding Tagged Pointers in iOS: Memory Savings and Performance Boosts

This article explains the Tagged Pointer mechanism introduced in iOS 64‑bit environments, detailing how it reduces memory usage, speeds up object creation and method dispatch, and describing the underlying tag‑plus‑data representation, class registration, and pointer generation with code examples.

Beike Product & Technology
Beike Product & Technology
Beike Product & Technology
Understanding Tagged Pointers in iOS: Memory Savings and Performance Boosts

1 Background

The iPhone 5s was the first device with a 64‑bit A7 dual‑core processor, and Apple introduced the Tagged Pointer concept to save memory and improve execution efficiency. In 64‑bit programs, Tagged Pointers can cut memory consumption by more than half, increase access speed threefold, and accelerate object creation/destruction up to 100×.

2 Without Tagged Pointers

Using NSNumber *a = @(1); as an example, a normal small object occupies at least 24 bytes (8‑byte pointer on the stack plus a 16‑byte heap object). The heap stores an isa pointer (8 bytes) and the integer value (4 bytes), but due to 16‑byte alignment the total is 16 bytes.

Performance-wise, an NSNumber requires dynamic memory allocation, reference‑count management, and a full objc_msgSend dispatch flow (message send, dynamic method resolution, message forwarding).

3 Using Tagged Pointers

3.1 Apple’s Description

Tagged Pointers are designed for small objects such as NSNumber, NSDate, NSString, etc.

The pointer value no longer represents an address; it directly encodes the actual value.

Memory reads are about three times faster, and creation is over 100× faster.

3.2 The Essence of Tagged Pointers

A Tagged Pointer is a pseudo‑pointer whose bits are split into a Tag and Data part.

Tag: a special marker that indicates the pointer is a Tagged Pointer and identifies the object type (NSNumber, NSDate, NSString, …).

Data: the actual value stored for the object.

In memory it occupies only a single 8‑byte pointer, saving a lot of overhead.

At the execution level, objc_msgSend first checks whether the pointer is Tagged; if so it returns the value directly without the normal message‑dispatch steps.

3.3 Current Status

Most small values (e.g., 4‑byte signed integers) fit within 8 bytes, so the majority of NSNumber/NSDate instances can be represented as Tagged Pointers; only values that exceed the capacity fall back to heap allocation.

4 Tagged Pointer Principle Analysis

4.1 Setting Environment Variables

Setting OBJC_DISABLE_TAG_OBFUSCATION=YES disables data obfuscation for Tagged Pointers. Setting OBJC_DISABLE_TAGGED_POINTERS=YES attempts to disable Tagged Pointers entirely (in older runtimes this caused a crash).

4.2 Example Analysis

// Close Tagged Pointer obfuscation
NSNumber *number1 = @(1);   // 0xb000000000000012
NSNumber *number2 = @(2);   // 0xb000000000000022
NSNumber *number3 = @(3);   // 0xb000000000000032
NSNumber *numberFFFF = @(0xFFFFFFFFFFFFFFFF); // 0x600000aa0b80

When obfuscation is disabled, the low‑order bits of the pointer directly contain the value (e.g., the trailing 12 encodes the integer 1). The high‑order nibble ( b ) indicates a Tagged Pointer and the class index (3 = NSNumber).

4.3 Parsing

The highest bit of the pointer (bit 63 on iOS) is the Tagged‑Pointer flag. The next three bits form the class index (e.g., 011 = 3 → NSNumber). The lowest four bits encode the data type (char = 0, short = 1, int = 2, long = 3, float = 4, double = 5).

4.3.1 Tag Identification

For a pointer like 0xb000000000000012 , the binary of b is 1011 : the most‑significant ‘1’ marks it as a Tagged Pointer, and the following 011 (decimal 3) identifies the NSNumber class.

4.3.2 Tag Index Bits

Apple’s source defines tag indices (e.g., OBJC_TAG_NSNumber = 3 , OBJC_TAG_NSString = 2 , OBJC_TAG_NSDate = 6 ) in objc_internal.h . These indices are used to look up the class during runtime.

4.3.3 Data Type Bits

The final hexadecimal digit represents the stored primitive type (char = 0, short = 1, int = 2, long = 3, float = 4, double = 5). Experiments with different C types confirm this mapping.

4.4 Source Annotation

The runtime comment (in objc-runtime-new.mm ) describes the layout:

/*
 * Tagged pointer objects store the class and the object value in the pointer itself.
 * LSB representation (macOS): 1‑bit tag, 3‑bit class index, 60‑bit payload.
 * MSB representation (iOS): 1‑bit tag, 3‑bit class index, 60‑bit payload.
 * Extended form (tag index 0b111) uses 8‑bit extended class index and a 52‑bit payload.
 */

Thus, the tag determines both the class and the payload size (60 bits for indices 0‑6, 52 bits for index 7 and above).

5 Creating Tagged Pointers

5.1 Initialization

During program start, _read_images calls initializeTaggedPointerObfuscator() , which either sets the global objc_debug_taggedpointer_obfuscator to zero (disabling obfuscation) or fills it with random data masked by ~_OBJC_TAG_MASK .

5.2 Registration and Validation

void _objc_registerTaggedPointerClass(objc_tag_index_t tag, Class cls) {
    if (objc_debug_taggedpointer_mask == 0) {
        _objc_fatal("tagged pointers are disabled");
    }
    Class *slot = classSlotForTagIndex(tag);
    if (!slot) {
        _objc_fatal("tag index %u is invalid", (unsigned int)tag);
    }
    Class oldCls = *slot;
    if (cls && oldCls && cls != oldCls) {
        _objc_fatal("tag index %u used for two different classes (was %p %s, now %p %s)",
                    tag, oldCls, oldCls->nameForLogging(), cls, cls->nameForLogging());
    }
    *slot = cls;
    if (tag < OBJC_TAG_First60BitPayload || tag > OBJC_TAG_Last60BitPayload) {
        Class *extSlot = classSlotForBasicTagIndex(OBJC_TAG_RESERVED_7);
        if (*extSlot == nil) {
            extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
            *extSlot = (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
        }
    }
}

This function ensures that each tag maps to a unique class and registers a placeholder for unrecognized extended tags.

Class Slot Lookup

static Class *classSlotForTagIndex(objc_tag_index_t tag) {
    if (tag >= OBJC_TAG_First60BitPayload && tag <= OBJC_TAG_Last60BitPayload) {
        return classSlotForBasicTagIndex(tag);
    }
    if (tag >= OBJC_TAG_First52BitPayload && tag <= OBJC_TAG_Last52BitPayload) {
        int index = tag - OBJC_TAG_First52BitPayload;
        uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator >> _OBJC_TAG_EXT_INDEX_SHIFT) & _OBJC_TAG_EXT_INDEX_MASK);
        return &objc_tag_ext_classes[index ^ tagObfuscator];
    }
    return nil;
}

It distinguishes between basic (60‑bit) and extended (52‑bit) payload ranges and applies the obfuscator to the extended index.

Basic Tag Slot

static Class *classSlotForBasicTagIndex(objc_tag_index_t tag) {
    uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK);
    uintptr_t obfuscatedTag = tag ^ tagObfuscator;
#if SUPPORT_MSB_TAGGED_POINTERS
    return &objc_tag_classes[0x8 | obfuscatedTag];
#else
    return &objc_tag_classes[(obfuscatedTag << 1) | 1];
#endif
}

The function XORs the tag with a masked part of the global obfuscator and then indexes into the objc_tag_classes array, handling the difference between macOS (LSB) and iOS (MSB) layouts.

5.3 Generating a Tagged Pointer

static inline void *_Nonnull _objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value) {
    if (tag <= OBJC_TAG_Last60BitPayload) {
        uintptr_t result = (_OBJC_TAG_MASK |
                            ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |
                            ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    } else {
        uintptr_t result = (_OBJC_TAG_EXT_MASK |
                            ((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
                            ((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    }
}

Depending on whether the tag belongs to the 60‑bit or 52‑bit payload range, the function assembles the final pointer by shifting the tag and value into their respective fields and then encodes it.

Encoding / Decoding

static inline void *_Nonnull _objc_encodeTaggedPointer(uintptr_t ptr) {
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
static inline uintptr_t _objc_decodeTaggedPointer(const void *_Nullable ptr) {
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

Both operations are simple XORs with the global obfuscator, providing lightweight data protection.

6 Tagged Pointer Usage Tips

Because a Tagged Pointer is not a real object, it has no isa pointer. Attempting to print its isa in LLDB results in an error; instead, use object_getClass or isKindOfClass: to query its type.

With this knowledge, developers can understand why small objects like NSNumber, NSDate, and NSString are stored as pseudo‑pointers, how the runtime distinguishes them, and how to work with them safely.

iosMemory OptimizationruntimeObjective‑CTagged Pointer
Beike Product & Technology
Written by

Beike Product & Technology

As Beike's official product and technology account, we are committed to building a platform for sharing Beike's product and technology insights, targeting internet/O2O developers and product professionals. We share high-quality original articles, tech salon events, and recruitment information weekly. Welcome to follow us.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.