Nat! bio photo

Nat!

Senior Mull

Twitter Github Twitch

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

mulle_objc_call.c#L671:

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

mulle_objc_call.c#L555:

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.

mulle_objc_call.c#L266:

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:

mulle_objc_class.h#L713:

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.

Name:
E-mail: (not published)
Website: