# How Tagged Pointer Optimization Works in mulle-objc: 32-bit vs 64-bit Architecture

> Discover how tagged pointer optimization works in mulle-objc, storing small objects in pointers to eliminate heap allocations and boost performance on 32-bit and 64-bit systems.

- Repository: [mulle-objc/mulle-objc-runtime](https://github.com/mulle-objc/mulle-objc-runtime)
- Tags: internals
- Published: 2026-03-07

---

**Tagged pointer optimization stores small objects like integers, floats, and short strings directly inside pointer values by using low-order bits as class tags and high bits as encoded payloads, eliminating heap allocations and reducing cache pressure.**

The mulle-objc runtime implements tagged pointer optimization to avoid memory allocation for tiny objects. Instead of allocating heap blocks for small values, the runtime embeds data directly into the pointer address itself, using bit-masking and shifting operations defined in [`src/mulle-objc-taggedpointer.h`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-taggedpointer.h). This article explains the bit-level mechanics, class registration, and extraction APIs based on the `mulle-objc/mulle-objc-runtime` source code.

## Bit-Level Architecture and Tag Layout

The runtime reserves the lowest bits of a pointer to store a **tag index** that identifies the tagged-pointer class, while the remaining high bits contain the actual **payload** data. The number of reserved bits depends on architecture word size.

### Platform-Specific Tag Layout

| Architecture | Reserved Low Bits | Bit Mask (`mulle_objc_get_taggedpointer_mask`) | Bit Shift (`mulle_objc_get_taggedpointer_shift`) |
|--------------|-------------------|------------------------------------------------|---------------------------------------------------|
| 32-bit | 2 bits (`0b11`) | `0x3` | `2` |
| 64-bit | 3 bits (`0b111`) | `0x7` | `3` |

These values are computed at compile time in [`src/mulle-objc-taggedpointer.h`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-taggedpointer.h) using `sizeof(uintptr_t)` to differentiate between 32-bit and 64-bit platforms:

```c
static inline unsigned int mulle_objc_get_taggedpointer_mask(void) {
   return (sizeof(uintptr_t) == sizeof(uint32_t) ? 0x3 : 0x7);
}
static inline unsigned int mulle_objc_get_taggedpointer_shift(void) {
   return (sizeof(uintptr_t) == sizeof(uint32_t) ? 2 : 3);
}

```

## Creating Tagged Pointers

For each payload type, the header provides creator functions that perform three operations: validation, shifting, and tagging. The **tag index** (ranging from `1` to the mask value) designates which Objective-C class handles the object at runtime.

The `mulle_objc_create_unsigned_taggedpointer` function demonstrates this pipeline:

```c
static inline void *mulle_objc_create_unsigned_taggedpointer(uintptr_t value,
                                                            unsigned int index)
{
    assert(index > 0 && index <= mulle_objc_get_taggedpointer_mask());
    assert(mulle_objc_taggedpointer_is_valid_unsigned_value(value));
    return (void *)((value << mulle_objc_get_taggedpointer_shift()) | index);
}

```

Specialized creators exist for signed integers, `float`, and `double` values, each handling the specific bit-twiddling required to encode IEEE-754 floating-point numbers into the available high bits.

## Registering Tagged-Pointer Classes

Before the runtime can dispatch messages to a tagged pointer, it must map the tag index to an Objective-C class. The function `_mulle_objc_universe_set_taggedpointerclass_at_index` registers this association in the runtime's global context (the **universe**):

```c
_mulle_objc_universe_set_taggedpointerclass_at_index(universe,
                                                      (struct _mulle_objc_class *)[NSNumber class],
                                                      0x1);

```

When the runtime encounters a pointer whose low bits match a registered index, it treats that pointer as an instance of the associated class. The test driver in `test-compiler/constantstring/taggedpointer.m` demonstrates this by registering distinct classes for 5-bit and 7-bit string encodings at specific tag indices.

## Extracting Payload and Runtime Dispatch

To decode a tagged pointer, the runtime provides getter functions that reverse the encoding process. The **tag index** is extracted via bitwise AND with the mask:

```c
unsigned int mulle_objc_taggedpointer_get_index(void *pointer) {
    return (unsigned int)((uintptr_t)pointer &
                          mulle_objc_get_taggedpointer_mask());
}

```

The **payload** is recovered by shifting right by the architecture-specific shift amount:

```c
uintptr_t mulle_objc_taggedpointer_get_unsigned_value(void *pointer) {
    uintptr_t v = (uintptr_t)pointer;
    assert(v & mulle_objc_get_taggedpointer_mask()); // ensure it is tagged
    return v >> mulle_objc_get_taggedpointer_shift();
}

```

Because the runtime knows which class corresponds to each tag index, it can send Objective-C messages to tagged pointers exactly as it would to heap-allocated objects. The class's method implementation interprets the payload bits—whether as an integer, a float, or a custom encoding like the 5-bit ASCII strings demonstrated in `taggedpointer.m`.

## Performance Benefits of Tagged Pointer Optimization

- **Zero-allocation**: Frequently-used small objects require no heap memory, reducing pressure on the allocator and improving CPU cache locality.
- **Fast identity checks**: Because the low bits are reserved, tagged pointers never collide with genuine heap addresses (which are aligned and have zeroed low bits), enabling instant identification.
- **Class-based dispatch**: The runtime maintains full Objective-C message dispatch capabilities, allowing tagged pointers to respond to methods via their registered classes without boxing or unboxing overhead.

## Complete Example: Small Integer Storage

This example registers `NSNumber` as the handler for tag index `0x1` and creates a tagged pointer holding the unsigned value `123`:

```c
#include <mulle-objc-runtime/mulle-objc-runtime.h>

int main(void)
{
    struct _mulle_objc_universe *universe = mulle_objc_global_get_universe(0);
    struct _mulle_objc_class *intClass = (struct _mulle_objc_class *)[NSNumber class];

    // Register class for tag index 0x1
    _mulle_objc_universe_set_taggedpointerclass_at_index(universe, intClass, 0x1);

    // Create a tagged pointer holding the unsigned value 123
    void *tp = mulle_objc_create_unsigned_taggedpointer(123, 0x1);

    // Extract and print the value
    printf("value = %lu\n", (unsigned long)mulle_objc_taggedpointer_get_unsigned_value(tp));
    return 0;
}

```

## Complete Example: Short Constant Strings

The test file `test-compiler/constantstring/taggedpointer.m` demonstrates encoding short ASCII strings using 5-bit character packing. The custom class interprets the payload bits through a decoding routine (`mulle_char5_decode_ascii`), allowing constant strings up to 12 characters (on 64-bit) to exist entirely within the pointer:

```objective-c
/* See test-compiler/constantstring/taggedpointer.m for full implementation */
void *s = mulle_objc_create_unsigned_taggedpointer(encoded_5bit_value, 0x1);
[s print];   // Implementation decodes and prints the string from the pointer

```

## Summary

- Tagged pointer optimization in mulle-objc embeds small objects directly into pointer values, avoiding heap allocation.
- **32-bit platforms** reserve 2 low bits (mask `0x3`, shift `2`); **64-bit platforms** reserve 3 low bits (mask `0x7`, shift `3`).
- The `mulle_objc_create_unsigned_taggedpointer` function validates payloads, shifts them left, and ORs the tag index into low bits.
- Classes must be registered via `_mulle_objc_universe_set_taggedpointerclass_at_index` to enable message dispatch for specific tag indices.
- Payload extraction uses `mulle_objc_taggedpointer_get_unsigned_value` and architecture-specific right-shifts.
- Key implementation files are [`src/mulle-objc-taggedpointer.h`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-taggedpointer.h) and `test-compiler/constantstring/taggedpointer.m`.

## Frequently Asked Questions

### What data types can be stored using tagged pointer optimization?

The mulle-objc runtime supports tagged pointers for **unsigned integers**, **signed integers**, **floats**, and **doubles** through dedicated creator functions in [`mulle-objc-taggedpointer.h`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/mulle-objc-taggedpointer.h). Additionally, custom classes can interpret the payload bits arbitrarily—for example, encoding short ASCII strings using 5-bit or 7-bit character packing schemes. The only constraint is that the encoded data must fit into the high bits remaining after the architecture-specific shift (30 bits on 32-bit, 61 bits on 64-bit).

### How does the runtime prevent collisions between tagged pointers and heap addresses?

Heap-allocated objects are aligned in memory, meaning their addresses always have **zeroed low bits** (aligned to at least 4 bytes on 32-bit or 8 bytes on 64-bit). Tagged pointers deliberately set these low bits to a **non-zero tag index** (values 1 through the mask). This creates a disjoint address space where any pointer with non-zero low bits is instantly recognizable as tagged, while pointers with zeroed low bits are valid heap references.

### What happens if a payload value is too large for the available high bits?

The creator functions validate payload size using assertions like `mulle_objc_taggedpointer_is_valid_unsigned_value`. If a value exceeds the capacity of the available bits (determined by the shift amount), the assertion fails during development. In production usage, callers must check payload bounds before attempting to create a tagged pointer; values that are too large must be allocated as full heap objects instead.

### Can you send Objective-C messages to tagged pointers?

Yes. Because the runtime maintains a registration mapping between **tag indices** and **Objective-C classes** via `_mulle_objc_universe_set_taggedpointerclass_at_index`, it can resolve the class for any tagged pointer at message-send time. The runtime extracts the tag index from the low bits, looks up the registered class, and dispatches the message to that class's method implementation, passing the tagged pointer as the self argument. The implementation then decodes the payload bits to access the underlying value.