Objective-C Internals: The Many Uses of isa
The Objective-C runtime optimizes performance by packing additional information into an object’s isa
pointer. This post compares the different packing mechanisms and discusses the various field values stored in the pointer value.
The first post in this series introduced the isa
pointer: an instance variable in every Objective-C object that points to its class object, which identifies the type of the object instance. The previous post cited the internal definition of this field (char isa_storage[sizeof(isa_t)]
) and mentioned the isa
field is deprecated. Now, we’ll explore how the runtime uses this field and its deprecation in more detail.
Background
In Apple’s 32-bit Objective-C runtime on iOS, macOS, and tvOS, the isa
field is just a pointer to the object’s class object. The following code from objc-private.h
, objc-object.h
, and objc-class.mm
shows the effective implementation (for these platforms) of the object_getClass()
runtime function, which gets the isa
class pointer value from an object instance.
// objc-private.h
union isa_t {
private:
Class cls;
public:
Class getClass(bool authenticated);
};
// objc-object.h
Class objc_object::getIsa() {
return ISA(); // bool argument defaulted to false
}
Class isa_t::getClass(bool) {
return cls;
}
Class objc_object::ISA(bool) {
return isa().getClass(false);
}
// objc-class.mm
Class object_getClass(id obj) {
if (obj) return obj->getIsa();
else return Nil;
}
Non-Pointer isa
Apple’s 64-bit Objective-C runtime (except for simulators on 64-bit Intel processors) and the Apple Watch Objective-C runtime use a “non-pointer isa
,” which packs additional information into unused pointer bits.
Setting additional bits in the pointer value changes the address it references and invalidates its value—the new value may not be an address in the process address space, it may cause unaligned memory access if dereferenced, etc. Hence the name “non-pointer isa
.”
Because the isa
field no longer stores only the class pointer value, it is marked as deprecated in the objc.h
public header to discourage direct use that may lead to undefined behavior. (The availability macro definition is in objc-api.h
.) The object_getClass()
and object_setClass()
functions are the official replacement for direct use of the isa
field.
// objc-api.h
#if !defined(OBJC_ISA_AVAILABILITY)
# define OBJC_ISA_AVAILABILITY __attribute__((deprecated))
#endif
// objc.h
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
Non-Pointer isa Variants
At the time of this writing, there are three implementations of the non-pointer isa
:
-
Packed
isa
(Apple Siliconarm64
-not-e
variant, 64-bit Intel) -
Packed
isa
with Pointer Authentication (Apple Siliconarm64e
variant) -
Indexed
isa
(Apple Watch)
The following table shows the additional fields packed into the unused class pointer bits for each non-pointer isa
variant:
Packed isa |
Packed isa + Ptr Auth |
Indexed isa |
|
---|---|---|---|
|
✓ |
✓ |
✓ |
|
✓ |
✓ |
✓ |
|
✓ |
x |
✓ |
|
✓ |
− |
− |
|
− |
✓ |
− |
|
− |
− |
✓ |
|
✓ |
✓ |
✓ |
|
✓ |
✓ |
✓ |
|
✓ |
✓ |
✓ |
|
✓ |
✓ |
✓ |
Legend:
-
✓ variant has this field
-
− field not applicable for variant
-
x variant does not have this field
Now, let’s explore the use of each field in the runtime and how they’re used to improve Objective-C runtime performance.
nonpointer
This is the least significant bit of the pointer payload (and therefore always zero for pointer values) and, if set, indicates the isa
value is the non-pointer variant. This field enables the runtime to use the legacy isa
-value-is-a-class-pointer behavior or the non-pointer-isa
optimizations on a per-class basis at run time for compatibility purposes.
On macOS, non-pointer isa
is disabled for any application linked for OS X 10.10 or earlier, as direct isa
usage was not deprecated until OS X 10.11.
if (dyld_get_active_platform() == PLATFORM_MACOS && !dyld_program_sdk_at_least(dyld_platform_version_macOS_10_11)) {
DisableNonpointerIsa = true;
}
If the main application executable has a __DATA,__objc_rawisa
section, the runtime disables the non-pointer isa
feature. Applications that may load plug-ins linked before the deprecation of direct isa
usage use this section to enable binary compatibility. (This is also macOS-only.)
for (EACH_HEADER) {
if (hi->mhdr()->filetype != MH_EXECUTE) continue;
unsigned long size;
if (getsectiondata(hi->mhdr(), "__DATA", "__objc_rawisa", &size)) {
DisableNonpointerIsa = true;
}
break; // assume only one MH_EXECUTE image
}
For all platforms, the Objective-C runtime disables the non-pointer isa
feature for the OS_object
class and its descendants because libdispatch
also uses the isa
pointer as a vtable.
else if (!hackedDispatch && 0 == strcmp(ro->getName(), "OS_object")) {
// hack for libdispatch et al - isa also acts as vtable pointer
hackedDispatch = true;
instancesRequireRawIsa = true;
}
has_assoc, has_cxx_dtor, weakly_referenced, and has_sidetable_rc
The primary purpose of these four fields is to determine if an object can use the fast deallocation path, which simply free()
s the object’s memory. Otherwise, the runtime must first perform additional bookkeeping before freeing the memory. From objc-object.h
:
void objc_object::rootDealloc() {
if (isTaggedPointer()) return;
if (isa().nonpointer &&
!isa().weakly_referenced &&
!isa().has_assoc &&
#if ISA_HAS_CXX_DTOR_BIT
!isa().has_cxx_dtor &&
#else
!isa().getClass(false)->hasCxxDtor() &&
#endif
!isa().has_sidetable_rc)
{
free(this);
} else {
object_dispose((id)this);
}
}
-
has_assoc
: Set if an object has an associated object created through the use of theobjc_setAssociatedObject()
runtime API. If an object has one or more associated objects, the runtime must remove the entries from its side table before freeing the object’s memory. -
has_cxx_dtor
: Set if the class or a superclass has a.cxx_destruct
method. If an Objective-C object has one or more instance variables with a C++ type[1], the runtime calls the class’s.cxx_construct
instance method to run any non-trivial constructors during object allocation (before anyinit
method). After thedealloc
method chain completes, the runtime calls the class’s.cxx_destruct
instance method to run any non-trivial destructors before freeing the object’s memory.-
When Automatic Reference Counting (ARC) is enabled, the compiler implements releasing of a class’s instance variables in its
.cxx_destruct
method, inhibiting the optimization to callfree()
directly. Theobject_dispose()
code path callsobjc_destructInstance()
, which uses the non-pointerisa
bits, if available, to elide unnecessary clean-up operations. -
Note this bit isn’t available when pointer authentication is enabled, but the information is available on the class object at the cost of an additional memory load.
-
-
weakly_referenced
: Set whenever a weak reference[2] to the object is created. Like associated objects, the runtime must remove the entries from its side table before freeing the object’s memory. -
has_sidetable_rc
: If the retain count has overflowed extra_rc, a side table stores the additional retain counts where, again, the runtime must remove the entries before freeing the object’s memory.
shiftcls and shiftcls_and_sig
These fields store the class pointer for the packed isa
variants. Class objects are always 8-byte aligned (either though layout in the binary image or via the standard allocator at runtime), so the least significant 3 bits are always 0. Thus, the “shift class” field stores the class pointer with the bottom 3 bits shifted away. This field also relies on knowledge of the maximum pointer value allowed by the virtual memory system, as storing the value in the bit field truncates the value’s high bits.
The runtime signs the class pointer on Apple Silicon with Pointer Authentication (and stores it in the shiftcls_and_sig
field). In addition to the lower and upper bounds of valid pointer values, this field also relies on knowledge of the bits used by pointer authentication.
indexcls
The Objective-C runtime on Apple Watch stores class pointers in an array and stores the class’s array index in the isa
’s indexcls
field. Indexes are assigned lazily at runtime, and the runtime falls back to using a pointer isa
if the array’s capacity (32,767 entries) is exhausted.
The Apple Watch ABI uses 32-bit pointers[3], which don’t have enough unused bits to store both the pointer value and packed bits. Using an array to store the class pointers reduces the number of bits required for the class identity, enabling the performance advantages of the non-pointer isa at the cost of some indirection.
magic
This field is unused by the runtime. The runtime exports constants for the magic mask and magic value, which the debugger uses to identify object instances with a non-pointer isa
to enable Objective-C debugging facilities.
extra_rc
The retain count of an object instance is stored in a side table when the non-pointer isa
feature is unavailable for the platform or class hierarchy. However, using a side table can be a performance bottleneck for many concurrent retains or releases, as each operation must acquire a lock. The non-pointer isa
feature reduces this contention by storing an instance’s retain count in this field.
If the retain count overflows the field, half of the retain count is moves to a side table, and half remains in this field. Moving only half of the count enables future releases to decrement the field value before acquiring the lock to access the counts in the side table.
The size of this field varies by platform. The following table notes the field’s size and maximum inline retain count value for each platform.
extra_rc Bit Count |
Max. Value | |
---|---|---|
Packed |
8 |
255 |
Packed |
19 |
524,287 |
Packed |
8 |
255 |
Indexed |
7 |
127 |