mulle-objc: divining the guts of message sending
Continued from mulle-objc: caches pivot - selectors mask - methods preload.
So with the knowledge of the previous two articles, let’s see how message sending actually works in mulle-objc.
Please be sure to have read mulle-objc: removing superflous ifs and mulle-objc: inlined messaging before going on, because I won’t duplicate their contents here.
There is a plethora of message sending routines available. The following table shows the most interesting ones. There are a few more used internally, for delayed class setup for example.
Name | Compiler | OptLevel | Description |
---|---|---|---|
mulle_objc_object_call |
Y | 0,s | -method |
mulle_objc_object_constant_methodid_call |
Y | 1 | -method, partially inlined |
mulle_objc_object_inline_constant_methodid_call |
Y | 2,3 | -method, even more inlined |
mulle_objc_object_call_classid |
Y | 0,s | [super method] |
mulle_objc_object_inline_call_classid |
Y | 1,2,3 | [super method] partially inlined |
mulle_objc_class_metacall_classid |
Y | 0,s | +method |
mulle_objc_class_inline_metacall_classid |
Y | 1,2,3 | +method, partially inlined |
mulle_objc_object_call_class |
N | N/A | A variation of mulle_objc_object_call , if the class of self is already known |
mulle_objc_object_call_class_empty_cache |
N | N/A | Special variation of mulle_objc_object_call_class used for empty caches. |
mulle_objc_object_variable_methodid_call |
N | N/A | Used for messages. When the selector is in a variable and therefore not a known constant. |
mulle_objc_object_inline_variable_methodid_call |
N | N/A | Partially inlining of above. |
mulle_objc_objects_call |
N | N/A | Call multiple objects with the same method. Useful for -makeObjectsPerformSelector: |
The “Compiler” column indicates if calls to this function are emitted by the compiler. The “OptLevel” >column shows at which optimization level (e.g. -O2) that function will be emitted.
In this article we will examine mulle_objc_object_call
, since this is the most basic message sending function. The optimized variants will be the subject of one or more future articles.
The guts of mulle_objc_object_call
void *mulle_objc_object_call( void *obj,
mulle_objc_methodid_t methodid,
void *parameter)
{
struct _mulle_objc_class *cls;
if( __builtin_expect( ! obj, 0))
return( obj);
cls = _mulle_objc_object_get_isa( obj);
return( (*cls->call)( obj, methodid, parameter, cls));
}
The __builtin_expect
gives the compiler a hint that the nil check for ! obj
is unlikely. I haven’t seen any compiler output so far, that suggest that this actually helps… but it doesn’t hurt that much either.
So there is the nil
check. Then we read isa
from the object. (See: mulle_objc: object layout, retain counting, finalize). This gives us the class of the object which contains the method-lists and the method-cache.
Finally we vector through the class to actually perform the message sending. In the general case this will be mulle_objc_object_call_class
.
The guts of mulle_objc_object_call_class
void *mulle_objc_object_call_class( void *obj,
mulle_objc_methodid_t methodid,
void *parameter,
struct _mulle_objc_class *cls)
{
mulle_objc_methodimplementation_t imp;
struct _mulle_objc_cache *cache;
struct _mulle_objc_cacheentry *entries;
struct _mulle_objc_cacheentry *entry;
mulle_objc_cache_uint_t mask;
mulle_objc_cache_uint_t offset;
assert( obj);
assert( methodid != MULLE_OBJC_NO_METHODID && methodid != MULLE_OBJC_INVALID_METHODID);
entries = _mulle_objc_cachepivot_atomic_get_entries( &cls->cachepivot.pivot);
cache = _mulle_objc_cacheentry_get_cache_from_entries( entries);
mask = cache->mask;
offset = (mulle_objc_cache_uint_t) methodid;
for(;;)
{
offset = offset & mask;
entry = (void *) &((char *) entries)[ offset];
if( entry->key.uniqueid == methodid)
{
imp = (mulle_objc_methodimplementation_t) _mulle_atomic_functionpointer_nonatomic_read( &entry->value.functionpointer);
/*->*/ return( (*imp)( obj, methodid, parameter));
}
if( ! entry->key.uniqueid)
/*->*/ return( _mulle_objc_object_unfailing_call_methodid( obj, methodid, parameter, cls));
offset += sizeof( struct _mulle_objc_cacheentry);
}
}
First you will notice some asserts
. Always be sure to compile your mulle-objc programs with -NDEBUG
when you want to release something.
Next up is the cache code as explained in the previous article. The cache entries and mask are retrieved and the selector is used to index into the cache. With the for
loop, the cache entries are searched until either the selector matches - and then imp
is called - or - when there is no match - the function defers to _mulle_objc_object_unfailing_call_methodid
.
The guts of _mulle_objc_object_unfailing_call_methodid
In general underscore prefixed functions do not assert or check their parameters. It is assumed that
_
functions are called by non-underscore functions, which only pass vetted arguments.
static void *_mulle_objc_object_unfailing_call_methodid( void *obj,
mulle_objc_methodid_t methodid,
void *parameter,
struct _mulle_objc_class *cls)
{
mulle_objc_methodimplementation_t imp;
struct _mulle_objc_runtime *runtime;
struct _mulle_objc_method *method;
struct _mulle_objc_cacheentry *entry;
method = _mulle_objc_class_unfailing_search_method( cls, methodid);
imp = _mulle_objc_method_get_implementation( method);
runtime = _mulle_objc_class_get_runtime( cls);
if( runtime->debug.trace.method_calls)
{
mulle_objc_class_trace_method_call( cls, methodid, obj, parameter, imp);
}
else
{
// some special classes may choose to never cache
if( ! _mulle_objc_class_get_state_bit( cls, MULLE_OBJC_ALWAYS_EMPTY_CACHE))
{
entry = _mulle_objc_class_fill_cache_with_method( cls, method, methodid);
if( entry)
entry->key.uniqueid = methodid; // overwrite in forward case
}
}
/*->*/
return( (*imp)( obj, methodid, parameter));
}
First the method is looked up in the class hierarchy using _mulle_objc_class_unfailing_search_method
. Let’s have a look at that:
static inline struct _mulle_objc_method *_mulle_objc_class_unfailing_search_method( struct _mulle_objc_class *cls, mulle_objc_methodid_t methodid)
{
struct _mulle_objc_method *method;
method = _mulle_objc_class_search_method( cls, methodid, NULL, cls->inheritance);
assert( ! method || method->descriptor.methodid == methodid);
if( ! method)
method = _mulle_objc_class_unfailing_get_or_search_forwardmethod( cls, methodid);
return( method);
}
_mulle_objc_class_unfailing_search_method
calls _mulle_objc_class_search_method
first. How _mulle_objc_class_search_method
works, was explained in detail in
mulle-objc: method searching and accidental override protection so it won’t be covered here.
If there isn’t a method with this selector, the function will then procure the -forward:
method for this class via _mulle_objc_class_unfailing_get_or_search_forwardmethod
. So a method is returned in both cases.
The unfailing hints, that functions will NOT return, if they can’t find what’s been asked of them.
Back to _mulle_objc_object_unfailing_call_methodid
, where we now have got a method for the selector. We now also have an implementation (imp
) for the method:
imp = _mulle_objc_method_get_implementation( method);
Then there is some support for tracing runtime calls [1] and finally the cache is updated with a new entry using:
entry = _mulle_objc_class_fill_cache_with_method( cls, method, methodid);
The way the cache is actually filled by that function would spawn another article. So I’ll leave at that.
Finally the imp
is called. And that ends the tour of the non-optimized message sending of mulle-objc.
[1] As you may notice the runtime doesn’t cache if you enabled tracing. This is deliberate. I endures that not only the first call is traced, since otherwise the cache would be in effect afterwards.
Continue to mulle-objc: investigating the pros and cons of inlining
mulle_objc_object_call
.
Post a comment
All comments are held for moderation; basic HTML formatting accepted.