How the Fast Class Table and Fast Method Table Improve Performance in mulle-objc-runtime
The mulle-objc-runtime accelerates critical messaging paths by replacing hash-table traversals with constant-time array indexing through compile-time generated fast class tables and per-class fast method tables.
The mulle-objc-runtime optimizes Objective-C message dispatch by implementing specialized lookup structures that eliminate dictionary searches for the most frequently accessed classes and methods. These fast class table and fast method table structures provide O(1) access times through direct pointer indexing rather than traditional runtime hashing. This article examines the implementation details in the source code to explain how these tables minimize cache pressure and reduce dispatch latency.
Fast Class Table Architecture
Global Class Pointer Array
The runtime maintains a universe-global array of 64 atomic class pointers defined by MULLE_OBJC_S_FASTCLASSES. This structure, declared in src/mulle-objc-fastclasstable.h, stores direct references to the most commonly accessed _mulle_objc_infraclass instances.
#define MULLE_OBJC_S_FASTCLASSES 64
struct _mulle_objc_fastclasstable
{
union _mulle_objc_atomicclasspointer_t classes[ MULLE_OBJC_S_FASTCLASSES];
};
Because the table resides in the global runtime state (the universe), it is allocated once and shared across all threads, eliminating per-thread allocation overhead.
Compile-Time Hash-to-Index Mapping
Instead of computing hashes at runtime, the runtime uses compile-time macros (MULLE_OBJC_FASTCLASSHASH_n) to map known class IDs directly to table indices. The inline function mulle_objc_get_fastclasstable_index() implements this as a dense switch statement:
int mulle_objc_get_fastclasstable_index( mulle_objc_classid_t classid )
{
switch( classid )
{
#ifdef MULLE_OBJC_FASTCLASSHASH_0
case MULLE_OBJC_CLASSID( MULLE_OBJC_FASTCLASSHASH_0 ) : return( 0);
#endif
// ... additional cases ...
}
return( -1 );
}
This approach guarantees that class ID resolution requires no hashing calculations or dictionary lookups during execution.
Atomic O(1) Class Retrieval
Once the index is determined, retrieving the class pointer requires a single atomic read operation. The inline accessor mulle_objc_fastclasstable_get_infraclass() performs this indirection:
static inline struct _mulle_objc_infraclass *
mulle_objc_fastclasstable_get_infraclass( struct _mulle_objc_fastclasstable *table,
unsigned int i )
{
return (struct _mulle_objc_infraclass *)
_mulle_atomic_pointer_read( &table->classes[ i].pointer );
}
By bypassing the generic mulle_objc_class_lookup_* machinery, class-side calls such as +[NSString stringWith…] execute in constant time without lock contention or cache misses associated with hash table probes.
Fast Method Table Implementation
Per-Class Method Slots
Each class embeds a struct _mulle_objc_fastmethodtable containing 24 atomic method pointer slots (MULLE_OBJC_S_FASTMETHODS). This structure caches the implementation addresses for high-frequency selectors including alloc, init, dealloc, and autorelease.
#define MULLE_OBJC_S_FASTMETHODS 24
struct _mulle_objc_fastmethodtable
{
union _mulle_objc_atomicmethodpointer_t methods[ MULLE_OBJC_S_FASTMETHODS];
};
Unlike the global fast class table, this structure is replicated per-class, allowing each class to maintain its own optimized dispatch path for inherited or overridden methods.
Fault Handler Initialization
At startup, _mulle_objc_fastmethodtable_init() populates all 24 slots with specialized fault handlers rather than actual method implementations. These handlers, generated by macros in src/mulle-objc-fastmethodtable.c, resolve the real implementation on first invocation:
static void *_mulle_objc_fastmethodtablefaulthandler_0( void *obj,
mulle_objc_methodid_t sel,
void *param )
{
return _mulle_objc_object_handle_fastmethodtablefault( obj, sel, param, 0 );
}
When a fast-method slot receives its first call, the fault handler performs the standard class lookup, caches the resulting pointer into the atomic slot (unless tracing is active), and forwards the current message. Subsequent calls bypass this resolution entirely.
Direct Dispatch After Priming
After the first successful dispatch, the slot contains a direct pointer to the method implementation. The dispatch path reduces to a single atomic read followed by an indirect call:
void *(*imp)( void *, mulle_objc_methodid_t, void * ) =
_mulle_atomic_pointer_read( &table->methods[ index].pointer );
return imp( obj, methodid, params );
This eliminates method list traversal and cache misses, providing branch prediction-friendly code paths for the most common Objective-C messages.
Performance Impact Analysis
The fast tables transform high-frequency operations from variable-time hash lookups into fixed-time pointer fetches:
- Class Lookup: Without fast tables, the runtime traverses hash tables with potential lock contention. With
mulle_objc_fastclasstable_get_infraclass(), the operation becomes a single pointer read. - Method Dispatch: Generic dispatch requires cache probing and potential method list walks. The fast method table provides direct slot access after the first resolution.
- Memory Footprint: The structures consume minimal memory—64 pointers globally and 24 pointers per class—keeping working sets within L1/L2 cache boundaries.
Summary
- The fast class table stores 64 class pointers in a global array indexed by compile-time class IDs, eliminating hash calculations for common classes.
- The fast method table embeds 24 method slots per class, using fault handlers to lazily resolve and cache high-frequency method implementations.
- Both structures rely on atomic pointer reads to provide thread-safe O(1) access without locks.
- These mechanisms reduce instruction count, improve CPU branch prediction, and minimize cache pressure during Objective-C message dispatch.
Frequently Asked Questions
What is the size of the fast class table in mulle-objc-runtime?
The fast class table contains 64 entries defined by MULLE_OBJC_S_FASTCLASSES in src/mulle-objc-fastclasstable.h. This fixed size provides a balance between coverage for common classes and cache locality.
How does the fast method table handle methods that are not in the 24 fast slots?
Selectors that do not map to the 24 fast method indices fall back to the standard method cache and lookup chain. The mulle_objc_get_fastmethodtable_index() function returns -1 for non-fast methods, triggering the conventional dispatch path through mulle_objc_object_handle_fastmethodtablefault().
Why use fault handlers instead of pre-populating the fast method table?
Fault handlers enable lazy initialization of method pointers, reducing startup overhead and memory pressure. The first invocation performs the expensive lookup and caches the result, while subsequent invocations execute the fast path. This design avoids resolving rarely-used methods during class loading.
What is the time complexity of fast table lookups?
Both fast class table and fast method table lookups operate in O(1) constant time. The class table uses compile-time index calculation followed by a single atomic read, while the method table uses a direct array index into the per-class structure after the initial fault resolution.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →