mick@next:~/objc/nstagged-pointer-string$/* Rediscovering the language that built the future */
NSTaggedPointerString - The Hidden Optimization You've Never Heard Of
objc / nstagged-pointer-string.m

NSTaggedPointerString - The Hidden Optimization You've Never Heard Of

Mick MacCallumMick MacCallum
@Objective-C@Memory@Performance

Here's something that might surprise you: when you create a short NSString in Objective-C, there's a good chance no object is actually allocated. The string's contents are stored directly in the pointer itself. Welcome to the world of tagged pointers.

What Is a Tagged Pointer?

On 64-bit systems, pointers have 64 bits, but most of those bits go unused. Valid heap addresses don't use the highest bits, so the runtime can repurpose them as a tag indicating "this isn't really a pointer—it's data pretending to be one."

When you write:

NSString *name = @"Hi";

The runtime doesn't allocate heap memory. Instead, it encodes "Hi" directly into the 64-bit value of name. The high bits mark it as a tagged pointer, and the rest store the actual characters.

Seeing It In Action

You can observe this with a bit of pointer inspection:

NSString *short1 = [[NSString alloc] initWithFormat:@"Hi"];
NSString *short2 = [[NSString alloc] initWithFormat:@"Hello"];
NSString *long1 = [[NSString alloc] initWithFormat:@"This is a longer string that won't fit"];

NSLog(@"short1 class: %@", [short1 class]);
NSLog(@"short2 class: %@", [short2 class]);
NSLog(@"long1 class: %@", [long1 class]);

You'll see something like:

short1 class: NSTaggedPointerString
short2 class: NSTaggedPointerString
long1 class: __NSCFString

The short strings become NSTaggedPointerString, while longer ones fall back to regular heap-allocated strings.

How Many Characters Fit?

The exact limit depends on the characters. ASCII characters pack more efficiently than Unicode. In practice, you can fit about 7-11 ASCII characters in a tagged pointer string. Try this experiment:

for (int i = 1; i <= 15; i++) {
    NSMutableString *str = [NSMutableString string];
    for (int j = 0; j < i; j++) {
        [str appendString:@"a"];
    }
    NSString *test = [str copy];
    NSLog(@"%2d chars: %@", i, [test class]);
}

You'll see the transition point where strings start requiring heap allocation.

Performance Implications

Tagged pointer strings avoid three significant costs:

First, no heap allocation. Creating a tagged pointer string is essentially free—just bit manipulation in registers. No malloc, no memory fragmentation.

Second, no reference counting. The runtime recognizes tagged pointers and skips retain/release operations entirely. The "object" has no reference count to update.

Third, no deallocation. When the pointer goes out of scope, there's nothing to free. No autorelease pool involvement, no dealloc, nothing.

For code that creates many small strings—think JSON parsing, string manipulation, logging—this adds up to substantial savings.

The Runtime Magic

When you call methods on a tagged pointer string, the runtime intercepts the message send. Instead of looking up methods in a class's method table, it recognizes the tagged pointer and dispatches to optimized implementations that extract the string data from the pointer value itself.

This means [taggedString length] or [taggedString characterAtIndex:0] never actually dereference a pointer to object memory. They decode the length and characters from the 64-bit value.

Why NSTaggedPointerString Is Invisible

Apple intentionally keeps this an implementation detail. You can't create tagged pointer strings explicitly, and you shouldn't check for them. The class is private, and its name could change in any OS release.

The beauty is that it just works. Your code doesn't need to know whether a string is tagged or heap-allocated. The abstraction is seamless.

// Both work identically
NSString *tagged = @"Hi";
NSString *heap = @"This is definitely too long to be a tagged pointer";

// Same API, same behavior
NSLog(@"Tagged length: %lu", tagged.length);
NSLog(@"Heap length: %lu", heap.length);

Other Tagged Pointer Types

Strings aren't alone. The runtime uses tagged pointers for:

  • NSNumber: Small integers and floats
  • NSDate: Dates within a certain range
  • NSIndexPath: Short paths with small indexes

Each has the same goal: avoid heap allocation for common small values.

Debugging Considerations

When debugging memory issues, be aware that tagged pointer objects won't appear in heap analysis tools. Instruments won't show allocations for them, because there aren't any. If you're tracking down a memory bug with strings, remember that many of your short strings aren't "real" objects.

You can disable tagged pointers for debugging with the environment variable OBJC_DISABLE_TAGGED_POINTERS=YES, though this is rarely necessary.

The Takeaway

Tagged pointer strings represent the runtime doing heavy optimization work behind the scenes so your code doesn't have to care. It's a perfect example of Objective-C's philosophy: the runtime should be smart so the developer can write natural code.

Every time you create a short string, there's a good chance you're benefiting from this invisible optimization. Millions of allocations never happen, reference counting operations are skipped, and memory stays clean—all without you writing a single line of optimization code.