Objective-C Implementation and Performance Details for C and C++ Programmers

1 Introduction

It is easy for C and C++ programmers to learn the "Objective" parts of Objective-C by treating them as new syntax for the same things they are used to doing in C or C++. While this is a great way to learn quickly, it can be misleading. The Objective-C runtime's features (and consequent performance characteristics) are arguably closer to scripting languages like Python than to C++. This article explains enough details of Objective-C implementation to give you a more correct mental model of how common constructs will perform.

In this text, I assume that you already know Objective-C reasonably well and that you are an efficiency-minded C++ or C programmer. You will need to be able to read assembly language to fully understand the examples. Sections are organized around Objective-C features. Each section will introduce its feature briefly, explain how it works on 64-bit iOS devices as of Xcode 8.3.3, and walk through an example.

1.1 Resources

This text will occasionally make reference to Apple's Objective-C runtime open source release. While I have included links inline to the most recent version at publication time, you may also find it useful to check out the unofficial GitHub mirror.

1.2 About the examples

All examples in this text are compiled with the following script (C++ examples substitute clang++):

#!/usr/bin/env bash

xcrun --sdk iphoneos clang -arch arm64 -S -Os $@

1.3 Disclaimer

I am publishing this article in my own personal capacity. The views expressed herein are my own and do not necessarily reflect those of my employer.

2 Class Metadata

Objective-C classes are defined in two major pieces: @interface and @implementation.

2.1 @interface vs. @implementation

@interface by itself produces no artifacts in the compiled program; it is for static checking only. For example, here is a file containing an unimplemented @interface that uses lots of features:

#import <Foundation/Foundation.h>

@interface Noop
{
@private
  NSString *privateIvar;
@protected
  NSString *protectedIvar;
@public
  NSString *publicIvar;
}
- (int)anInstanceMethod;
+ (float)aClassMethod;

@property (atomic, readwrite, copy) NSString *aProperty;

@end

When we compile it, we can see that it generates no actual code:

        .section        __TEXT,__text,regular,pure_instructions
        .ios_version_min 10, 3
        .section        __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
        .long   0
        .long   64


.subsections_via_symbols

(Similar content appears in the assembly for the rest of the examples, but I've omitted it for brevity.)

2.2 Class artifacts

@implementation, then, is what really triggers class artifacts to start showing up. How is each thing we can add to a class represented?

2.2.1 Empty class

Let's first consider a simple empty class:

#import <Foundation/Foundation.h>

@interface SomeClass : NSObject
@end

@implementation SomeClass
@end

Here is what the compiler produces given this definition:

  • C-string name of the class (L_OBJC_CLASS_NAME_)
  • 5-word struct objc_class (see objc-runtime-new.h) describing the class and another one for its metaclass (_OBJC_CLASS_$_SomeClass and _OBJC_METACLASS_$_SomeClass)
  • 9-word struct class_ro_t (see objc-runtime-new.h) for the class & another one for its metaclass (l_OBJC_CLASS_RO_$_SomeClass and l_OBJC_METACLASS_RO_$_SomeClass)
  • Pointer to the class in the __DATA.__objc_classlist section

In short, on a 64-bit machine, each class @implementation will cause the compiler to emit the class name, at least 224 bytes of metadata structures, and a pointer to one of the metadata structures being emitted.

Here is the full assembly:

        .section        __TEXT,__objc_classname,cstring_literals
L_OBJC_CLASS_NAME_:                     ; @OBJC_CLASS_NAME_
        .asciz  "SomeClass"

        .section        __DATA,__objc_const
        .p2align        3               ; @"\01l_OBJC_METACLASS_RO_$_SomeClass"
l_OBJC_METACLASS_RO_$_SomeClass:
        .long   1                       ; 0x1
        .long   40                      ; 0x28
        .long   40                      ; 0x28
        .space  4
        .quad   0
        .quad   L_OBJC_CLASS_NAME_
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   0

        .section        __DATA,__objc_data
        .globl  _OBJC_METACLASS_$_SomeClass ; @"OBJC_METACLASS_$_SomeClass"
        .p2align        3
_OBJC_METACLASS_$_SomeClass:
        .quad   _OBJC_METACLASS_$_NSObject
        .quad   _OBJC_METACLASS_$_NSObject
        .quad   __objc_empty_cache
        .quad   0
        .quad   l_OBJC_METACLASS_RO_$_SomeClass

        .section        __DATA,__objc_const
        .p2align        3               ; @"\01l_OBJC_CLASS_RO_$_SomeClass"
l_OBJC_CLASS_RO_$_SomeClass:
        .long   0                       ; 0x0
        .long   8                       ; 0x8
        .long   8                       ; 0x8
        .space  4
        .quad   0
        .quad   L_OBJC_CLASS_NAME_
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   0

        .section        __DATA,__objc_data
        .globl  _OBJC_CLASS_$_SomeClass ; @"OBJC_CLASS_$_SomeClass"
        .p2align        3
_OBJC_CLASS_$_SomeClass:
        .quad   _OBJC_METACLASS_$_SomeClass
        .quad   _OBJC_CLASS_$_NSObject
        .quad   __objc_empty_cache
        .quad   0
        .quad   l_OBJC_CLASS_RO_$_SomeClass

        .section        __DATA,__objc_classlist,regular,no_dead_strip
        .p2align        3               ; @"OBJC_LABEL_CLASS_$"
L_OBJC_LABEL_CLASS_$:
        .quad   _OBJC_CLASS_$_SomeClass

2.2.2 Ivars

What if we add an ivar?

#import <Foundation/Foundation.h>

@interface SomeClass : NSObject
{
  NSString *anIvar;
}
@end

@implementation SomeClass
@end

The name and type of the ivar will be emitted as C strings, and referenced in the new struct ivar_t (see objc-runtime-new.h) ivar list for the class. We also get a new struct ivar_list_t (l_OBJC_$_INSTANCE_VARIABLES_SomeClass) ), which contains our new struct ivar_t plus 8 bytes of overhead, and our struct objc_class now points to l_OBJC_$_INSTANCE_VARIABLES_SomeClass:

        .section        __TEXT,__objc_classname,cstring_literals
L_OBJC_CLASS_NAME_:                     ; @OBJC_CLASS_NAME_
        .asciz  "SomeClass"

        .section        __DATA,__objc_const
        .p2align        3               ; @"\01l_OBJC_METACLASS_RO_$_SomeClass"
l_OBJC_METACLASS_RO_$_SomeClass:
        .long   1                       ; 0x1
        .long   40                      ; 0x28
        .long   40                      ; 0x28
        .space  4
        .quad   0
        .quad   L_OBJC_CLASS_NAME_
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   0

        .section        __DATA,__objc_data
        .globl  _OBJC_METACLASS_$_SomeClass ; @"OBJC_METACLASS_$_SomeClass"
        .p2align        3
_OBJC_METACLASS_$_SomeClass:
        .quad   _OBJC_METACLASS_$_NSObject
        .quad   _OBJC_METACLASS_$_NSObject
        .quad   __objc_empty_cache
        .quad   0
        .quad   l_OBJC_METACLASS_RO_$_SomeClass

        .section        __DATA,__objc_ivar
        .globl  _OBJC_IVAR_$_SomeClass.anIvar ; @"OBJC_IVAR_$_SomeClass.anIvar"
        .p2align        2
_OBJC_IVAR_$_SomeClass.anIvar:
        .long   8                       ; 0x8

        .section        __TEXT,__objc_methname,cstring_literals
L_OBJC_METH_VAR_NAME_:                  ; @OBJC_METH_VAR_NAME_
        .asciz  "anIvar"

        .section        __TEXT,__objc_methtype,cstring_literals
L_OBJC_METH_VAR_TYPE_:                  ; @OBJC_METH_VAR_TYPE_
        .asciz  "@\"NSString\""

        .section        __DATA,__objc_const
        .p2align        3               ; @"\01l_OBJC_$_INSTANCE_VARIABLES_SomeClass"
l_OBJC_$_INSTANCE_VARIABLES_SomeClass:
        .long   32                      ; 0x20
        .long   1                       ; 0x1
        .quad   _OBJC_IVAR_$_SomeClass.anIvar
        .quad   L_OBJC_METH_VAR_NAME_
        .quad   L_OBJC_METH_VAR_TYPE_
        .long   3                       ; 0x3
        .long   8                       ; 0x8

        .p2align        3               ; @"\01l_OBJC_CLASS_RO_$_SomeClass"
l_OBJC_CLASS_RO_$_SomeClass:
        .long   0                       ; 0x0
        .long   8                       ; 0x8
        .long   16                      ; 0x10
        .space  4
        .quad   0
        .quad   L_OBJC_CLASS_NAME_
        .quad   0
        .quad   0
        .quad   l_OBJC_$_INSTANCE_VARIABLES_SomeClass
        .quad   0
        .quad   0

        .section        __DATA,__objc_data
        .globl  _OBJC_CLASS_$_SomeClass ; @"OBJC_CLASS_$_SomeClass"
        .p2align        3
_OBJC_CLASS_$_SomeClass:
        .quad   _OBJC_METACLASS_$_SomeClass
        .quad   _OBJC_CLASS_$_NSObject
        .quad   __objc_empty_cache
        .quad   0
        .quad   l_OBJC_CLASS_RO_$_SomeClass

        .section        __DATA,__objc_classlist,regular,no_dead_strip
        .p2align        3               ; @"OBJC_LABEL_CLASS_$"
L_OBJC_LABEL_CLASS_$:
        .quad   _OBJC_CLASS_$_SomeClass

Note that when Objective-C++ is in use, the full "compiler error message" version of the name of any class types used in ivars will be emitted. This can be quite large if we use a C++ template class like std::unordered_map:

#import <Foundation/Foundation.h>

#import <string>
#import <unordered_map>

@interface SomeClass : NSObject
{
  std::unordered_map<std::string, std::string> aHugeIvar;
}
@end

@implementation SomeClass
@end
.asciz  "{unordered_map<std::__1::basic_string<char>, std::__1::basic_string<char>, std::__1::hash<std::__1::basic_string<char> >, std::__1::equal_to<std::__1::basic_string<char> >, std::__1::allocator<std::__1::pair<const std::__1::basic_string<char>, std::__1::basic_string<char> > > >=\"__table_\"{__hash_table<std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, std::__1::__unordered_map_hasher<std::__1::basic_string<char>, std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, std::__1::hash<std::__1::basic_string<char> >, true>, std::__1::__unordered_map_equal<std::__1::basic_string<char>, std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, std::__1::equal_to<std::__1::basic_string<char> >, true>, std::__1::allocator<std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> > > >=\"__bucket_list_\"{unique_ptr<std::__1::__hash_node_base<std::__1::__hash_node<std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, void *> *> *[], std::__1::__bucket_list_deallocator<std::__1::allocator<std::__1::__hash_node_base<std::__1::__hash_node<std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, void *> *> *> > >=\"__ptr_\"{__compressed_pair<std::__1::__hash_node_base<std::__1::__hash_node<std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, void *> *> **, std::__1::__bucket_list_deallocator<std::__1::allocator<std::__1::__hash_node_base<std::__1::__hash_node<std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, void *> *> *> > >=\"__first_\"^^{__hash_node_base<std::__1::__hash_node<std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, void *> *>}\"__second_\"{__bucket_list_deallocator<std::__1::allocator<std::__1::__hash_node_base<std::__1::__hash_node<std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, void *> *> *> >=\"__data_\"{__compressed_pair<unsigned long, std::__1::allocator<std::__1::__hash_node_base<std::__1::__hash_node<std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, void *> *> *> >=\"__first_\"Q}}}}\"__p1_\"{__compressed_pair<std::__1::__hash_node_base<std::__1::__hash_node<std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, void *> *>, std::__1::allocator<std::__1::__hash_node<std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, void *> > >=\"__first_\"{__hash_node_base<std::__1::__hash_node<std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, void *> *>=\"__next_\"^{__hash_node_base<std::__1::__hash_node<std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, void *> *>}}}\"__p2_\"{__compressed_pair<unsigned long, std::__1::__unordered_map_hasher<std::__1::basic_string<char>, std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, std::__1::hash<std::__1::basic_string<char> >, true> >=\"__first_\"Q}\"__p3_\"{__compressed_pair<float, std::__1::__unordered_map_equal<std::__1::basic_string<char>, std::__1::__hash_value_type<std::__1::basic_string<char>, std::__1::basic_string<char> >, std::__1::equal_to<std::__1::basic_string<char> >, true> >=\"__first_\"f}}}"

(It is not clear to me what purpose this is intended to serve.)

2.2.3 Methods

Let's add a method:

#import <Foundation/Foundation.h>

@interface SomeClass : NSObject
- (void)doSomething;
@end

@implementation SomeClass
- (void)doSomething {}
@end

The code generation is extremely similar to what we would get if we added a new ivar, except we also have code emitted for the new method. The name and signature of the method will be emitted as C strings, referenced in the new struct method_t for the method. We also get a new struct method_list_t (l_OBJC_$_INSTANCE_METHODS_SomeClass:) with 8 bytes of overhead that is referenced by our struct objc_class.

        .p2align        2
"-[SomeClass doSomething]":             ; @"\01-[SomeClass doSomething]"
; BB#0:
        ret

        .section        __TEXT,__objc_classname,cstring_literals
L_OBJC_CLASS_NAME_:                     ; @OBJC_CLASS_NAME_
        .asciz  "SomeClass"

        .section        __DATA,__objc_const
        .p2align        3               ; @"\01l_OBJC_METACLASS_RO_$_SomeClass"
l_OBJC_METACLASS_RO_$_SomeClass:
        .long   1                       ; 0x1
        .long   40                      ; 0x28
        .long   40                      ; 0x28
        .space  4
        .quad   0
        .quad   L_OBJC_CLASS_NAME_
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   0

        .section        __DATA,__objc_data
        .globl  _OBJC_METACLASS_$_SomeClass ; @"OBJC_METACLASS_$_SomeClass"
        .p2align        3
_OBJC_METACLASS_$_SomeClass:
        .quad   _OBJC_METACLASS_$_NSObject
        .quad   _OBJC_METACLASS_$_NSObject
        .quad   __objc_empty_cache
        .quad   0
        .quad   l_OBJC_METACLASS_RO_$_SomeClass

        .section        __TEXT,__objc_methname,cstring_literals
L_OBJC_METH_VAR_NAME_:                  ; @OBJC_METH_VAR_NAME_
        .asciz  "doSomething"

        .section        __TEXT,__objc_methtype,cstring_literals
L_OBJC_METH_VAR_TYPE_:                  ; @OBJC_METH_VAR_TYPE_
        .asciz  "v16@0:8"

        .section        __DATA,__objc_const
        .p2align        3               ; @"\01l_OBJC_$_INSTANCE_METHODS_SomeClass"
l_OBJC_$_INSTANCE_METHODS_SomeClass:
        .long   24                      ; 0x18
        .long   1                       ; 0x1
        .quad   L_OBJC_METH_VAR_NAME_
        .quad   L_OBJC_METH_VAR_TYPE_
        .quad   "-[SomeClass doSomething]"

        .p2align        3               ; @"\01l_OBJC_CLASS_RO_$_SomeClass"
l_OBJC_CLASS_RO_$_SomeClass:
        .long   0                       ; 0x0
        .long   8                       ; 0x8
        .long   8                       ; 0x8
        .space  4
        .quad   0
        .quad   L_OBJC_CLASS_NAME_
        .quad   l_OBJC_$_INSTANCE_METHODS_SomeClass
        .quad   0
        .quad   0
        .quad   0
        .quad   0

        .section        __DATA,__objc_data
        .globl  _OBJC_CLASS_$_SomeClass ; @"OBJC_CLASS_$_SomeClass"
        .p2align        3
_OBJC_CLASS_$_SomeClass:
        .quad   _OBJC_METACLASS_$_SomeClass
        .quad   _OBJC_CLASS_$_NSObject
        .quad   __objc_empty_cache
        .quad   0
        .quad   l_OBJC_CLASS_RO_$_SomeClass

        .section        __DATA,__objc_classlist,regular,no_dead_strip
        .p2align        3               ; @"OBJC_LABEL_CLASS_$"
L_OBJC_LABEL_CLASS_$:
        .quad   _OBJC_CLASS_$_SomeClass

The same caveats about Objective-C++ that apply to ivars apply to methods too.

2.2.4 Properties

Finally, how about a property?

#import <Foundation/Foundation.h>

@interface SomeClass : NSObject
@property (readonly, nonatomic, retain) NSString *aString;
@end

@implementation SomeClass
@end

We get code emitted for the backing ivar (_aString) and backing getter (-[SomeClass aString]) for our property, as covered in the previous sections. We also get an extra C string describing our property's attributes, which is referenced in the new struct property_t, and we also have a new struct property_list_t because we just added the first property. Again, see objc-runtime-new.h for structure definitions.

        .p2align        2
"-[SomeClass aString]":                 ; @"\01-[SomeClass aString]"
; BB#0:
Lloh0:
        adrp    x8, _OBJC_IVAR_$_SomeClass._aString@PAGE
Lloh1:
        ldrsw   x8, [x8, _OBJC_IVAR_$_SomeClass._aString@PAGEOFF]
        ldr             x0, [x0, x8]
        ret
        .loh AdrpLdr    Lloh0, Lloh1

        .private_extern _OBJC_IVAR_$_SomeClass._aString ; @"OBJC_IVAR_$_SomeClass._aString"
        .section        __DATA,__objc_ivar
        .globl  _OBJC_IVAR_$_SomeClass._aString
        .p2align        2
_OBJC_IVAR_$_SomeClass._aString:
        .long   8                       ; 0x8

        .section        __TEXT,__objc_classname,cstring_literals
L_OBJC_CLASS_NAME_:                     ; @OBJC_CLASS_NAME_
        .asciz  "SomeClass"

        .section        __DATA,__objc_const
        .p2align        3               ; @"\01l_OBJC_METACLASS_RO_$_SomeClass"
l_OBJC_METACLASS_RO_$_SomeClass:
        .long   1                       ; 0x1
        .long   40                      ; 0x28
        .long   40                      ; 0x28
        .space  4
        .quad   0
        .quad   L_OBJC_CLASS_NAME_
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   0

        .section        __DATA,__objc_data
        .globl  _OBJC_METACLASS_$_SomeClass ; @"OBJC_METACLASS_$_SomeClass"
        .p2align        3
_OBJC_METACLASS_$_SomeClass:
        .quad   _OBJC_METACLASS_$_NSObject
        .quad   _OBJC_METACLASS_$_NSObject
        .quad   __objc_empty_cache
        .quad   0
        .quad   l_OBJC_METACLASS_RO_$_SomeClass

        .section        __TEXT,__objc_methname,cstring_literals
L_OBJC_METH_VAR_NAME_:                  ; @OBJC_METH_VAR_NAME_
        .asciz  "aString"

        .section        __TEXT,__objc_methtype,cstring_literals
L_OBJC_METH_VAR_TYPE_:                  ; @OBJC_METH_VAR_TYPE_
        .asciz  "@16@0:8"

        .section        __DATA,__objc_const
        .p2align        3               ; @"\01l_OBJC_$_INSTANCE_METHODS_SomeClass"
l_OBJC_$_INSTANCE_METHODS_SomeClass:
        .long   24                      ; 0x18
        .long   1                       ; 0x1
        .quad   L_OBJC_METH_VAR_NAME_
        .quad   L_OBJC_METH_VAR_TYPE_
        .quad   "-[SomeClass aString]"

        .section        __TEXT,__objc_methname,cstring_literals
L_OBJC_METH_VAR_NAME_.1:                ; @OBJC_METH_VAR_NAME_.1
        .asciz  "_aString"

        .section        __TEXT,__objc_methtype,cstring_literals
L_OBJC_METH_VAR_TYPE_.2:                ; @OBJC_METH_VAR_TYPE_.2
        .asciz  "@\"NSString\""

        .section        __DATA,__objc_const
        .p2align        3               ; @"\01l_OBJC_$_INSTANCE_VARIABLES_SomeClass"
l_OBJC_$_INSTANCE_VARIABLES_SomeClass:
        .long   32                      ; 0x20
        .long   1                       ; 0x1
        .quad   _OBJC_IVAR_$_SomeClass._aString
        .quad   L_OBJC_METH_VAR_NAME_.1
        .quad   L_OBJC_METH_VAR_TYPE_.2
        .long   3                       ; 0x3
        .long   8                       ; 0x8

        .section        __TEXT,__cstring,cstring_literals
L_OBJC_PROP_NAME_ATTR_:                 ; @OBJC_PROP_NAME_ATTR_
        .asciz  "aString"

L_OBJC_PROP_NAME_ATTR_.3:               ; @OBJC_PROP_NAME_ATTR_.3
        .asciz  "T@\"NSString\",R,&,N,V_aString"

        .section        __DATA,__objc_const
        .p2align        3               ; @"\01l_OBJC_$_PROP_LIST_SomeClass"
l_OBJC_$_PROP_LIST_SomeClass:
        .long   16                      ; 0x10
        .long   1                       ; 0x1
        .quad   L_OBJC_PROP_NAME_ATTR_
        .quad   L_OBJC_PROP_NAME_ATTR_.3

        .p2align        3               ; @"\01l_OBJC_CLASS_RO_$_SomeClass"
l_OBJC_CLASS_RO_$_SomeClass:
        .long   0                       ; 0x0
        .long   8                       ; 0x8
        .long   16                      ; 0x10
        .space  4
        .quad   0
        .quad   L_OBJC_CLASS_NAME_
        .quad   l_OBJC_$_INSTANCE_METHODS_SomeClass
        .quad   0
        .quad   l_OBJC_$_INSTANCE_VARIABLES_SomeClass
        .quad   0
        .quad   l_OBJC_$_PROP_LIST_SomeClass

        .section        __DATA,__objc_data
        .globl  _OBJC_CLASS_$_SomeClass ; @"OBJC_CLASS_$_SomeClass"
        .p2align        3
_OBJC_CLASS_$_SomeClass:
        .quad   _OBJC_METACLASS_$_SomeClass
        .quad   _OBJC_CLASS_$_NSObject
        .quad   __objc_empty_cache
        .quad   0
        .quad   l_OBJC_CLASS_RO_$_SomeClass

        .section        __DATA,__objc_classlist,regular,no_dead_strip
        .p2align        3               ; @"OBJC_LABEL_CLASS_$"
L_OBJC_LABEL_CLASS_$:
        .quad   _OBJC_CLASS_$_SomeClass

The extra property metadata is accessible with the Objective-C runtime. For example, we could get the property by name with class_getProperty and we could get the attribute string with property_getAttributes. If we knew we would never want to do this, it would be better for code size to manually write an ivar and getter rather than using @property.

3 Ivar Access

Ivars are not quite as efficient to access as C or C++ struct members. Recall that struct member access is as simple as loading from an offset into the struct:

struct SomeStruct {
  double dbl; // Added to make it more clear how offsets are handled by
              // giving x a nonzero offset.
  int x;
  int y;
};

int accessMember(struct SomeStruct *o)
{
  return o->x + o->y;
}

int accessArray(int o[4]) {
  return o[2] + o[3];
}
        .globl  _accessMember
        .p2align        2
_accessMember:                          ; @accessMember
; BB#0:
        ldp     w8, w9, [x0, #8]
        add             w0, w9, w8
        ret

        .globl  _accessArray
        .p2align        2
_accessArray:                           ; @accessArray
; BB#0:
        ldp     w8, w9, [x0, #8]
        add             w0, w9, w8
        ret

To read x and y, accessStruct just loads from offsets 8 and 12 at the location pointed to by o. (As it happens, the ldp ("Load Pair") instruction lets us load two ints at once in this case.) This case is exactly the same as if we were to pass an array of ints, as in accessArray.

Compare this to ivar access:

#import <Foundation/Foundation.h>

@interface SomeClass : NSObject
{
  int x;
  int y;
}
@end

@implementation SomeClass
- (int)accessIvar
{
  return x + y;
}
@end
"-[SomeClass accessIvar]":              ; @"\01-[SomeClass accessIvar]"
; BB#0:
Lloh0:
        adrp    x8, _OBJC_IVAR_$_SomeClass.x@PAGE
Lloh1:
        ldrsw   x8, [x8, _OBJC_IVAR_$_SomeClass.x@PAGEOFF]
        ldr             w8, [x0, x8]
Lloh2:
        adrp    x9, _OBJC_IVAR_$_SomeClass.y@PAGE
Lloh3:
        ldrsw   x9, [x9, _OBJC_IVAR_$_SomeClass.y@PAGEOFF]
        ldr             w9, [x0, x9]
        add             w0, w9, w8
        ret

Instead of one load per struct member access (previously combined into ldp), we are now looking at a load of a PC-relative pointer offset (adrp (PC-relative address of 4KB page) and ldrsw (load register signed word)), and a second load (ldr) using that offset into self (which is in x0).

In pseudo-C, this is something like

ptrdiff_t xOffset = SomeClass_x_ivar->offset;
int x = *(int *)((char *)self + xOffset);
ptrdiff_t yOffset = SomeClass_y_ivar->offset
int y = *(int *)((char *)self + yOffset);
return x + y;

For C++ experts, the code generated for ivar access is virtually identical to the code generated for the following contrived example of access via pointer-to-member:

struct SomeStruct {
  double dbl;
  int x;
  int y;
};

int SomeStruct::*xPtr = &SomeStruct::x;
int SomeStruct::*yPtr = &SomeStruct::y;

int accessViaMemPtr(SomeStruct *o) {
  return o->*xPtr + o->*yPtr;
}

Assembly:

__Z15accessViaMemPtrP10SomeStruct:      ; @_Z15accessViaMemPtrP10SomeStruct
; BB#0:
Lloh0:
        adrp    x8, _xPtr@PAGE
Lloh1:
        ldr     x8, [x8, _xPtr@PAGEOFF]
        ldr             w8, [x0, x8]
Lloh2:
        adrp    x9, _yPtr@PAGE
Lloh3:
        ldr     x9, [x9, _yPtr@PAGEOFF]
        ldr             w9, [x0, x9]
        add             w0, w9, w8
        ret
        .loh AdrpLdr    Lloh0, Lloh1
        .loh AdrpLdr    Lloh2, Lloh3

        .section        __DATA,__data
        .globl  _xPtr                   ; @xPtr
        .p2align        3
_xPtr:
        .quad   8                       ; 0x8

        .globl  _yPtr                   ; @yPtr
        .p2align        3
_yPtr:
        .quad   12                      ; 0xc

This article explains that the reason for the extra indirection in ivar access is to solve the fragile base class problem: unlike in C++, if a superclass has more ivars added, subclasses don't have to be recompiled to adjust their ivar offsets. Instead, the Objective-C runtime can just make this adjustment at load time.

4 Functions and Methods

If you're coming from C++, you may have assumed that Objective-C method calls are implemented similarly to virtual method calls. However, this assumption is misleading. The differences in behavior all stem from the call-by-name semantics of Objective-C calls, together with the dynamic nature of the language.

4.1 Instance method invocation

Let's start by comparing the code the compiler generates for a simple method call in Objective-C to the code we'd get in C or C++. To avoid dealing with assembly, let's treat a C function call as our baseline and describe C++ and Objective-C calls in terms of C.

When we call a virtual function on an object like o->doStuff(), clang uses the classic vtable approach. (If you're unfamiliar with it, o->doStuff() compiles to roughly o->vtbl->doStuffFunctionPointer(), where o->vtbl is a pointer to a structure containing one function pointer for each virtual function.) At the assembly level, this is roughly a load and an indirect jump.

Compare that to an example Objective-C call:

void callIndirect(id o) {
  [o doStuff];
}

Assembly:

_callIndirect:                          ; @callIndirect
        .cfi_startproc
; BB#0:
Lloh0:
        adrp    x8, L_OBJC_SELECTOR_REFERENCES_@PAGE
Lloh1:
        ldr     x1, [x8, L_OBJC_SELECTOR_REFERENCES_@PAGEOFF]
        b       _objc_msgSend
        .loh AdrpLdr    Lloh0, Lloh1
        .cfi_endproc

        .section        __TEXT,__objc_methname,cstring_literals
L_OBJC_METH_VAR_NAME_:                  ; @OBJC_METH_VAR_NAME_
        .asciz  "doStuff"

        .section        __DATA,__objc_selrefs,literal_pointers,no_dead_strip
        .p2align        3               ; @OBJC_SELECTOR_REFERENCES_
L_OBJC_SELECTOR_REFERENCES_:
        .quad   L_OBJC_METH_VAR_NAME_

So, [o doStuff] is roughly equivalent to objc_msgSend(o, OBJC_SELECTOR_REFERENCES[DO_STUFF_OFFSET]), where OBJC_SELECTOR_REFERENCES is a large array of pointers to selectors and DO_STUFF_OFFSET is an integer constant. At the assembly level, this is also roughly a load and an indirect (because of dynamic linking) jump. However, we have additional overhead in objc_msgSend at runtime!

There are entire articles on objc_msgSend that cover the topic much better than I could. In brief, objc_msgSend has two phases:

  1. Look up the selector in the cache of the object's class's recently used methods. (The cache is an open-addressing hash table with a really simple hash function: `hash(x) = x`. It will grow to accommodate all called methods.) If found, we're done.
  2. Binary search the object's class's method list. If found, we're done. If not found, try the superclass and repeat this step.

4.2 Class method invocation

If you write a lot of Objective-C code, eventually you may find that you habitually use class methods. In C++, static methods are efficient: they are called just like C functions. In Objective-C, class methods are a relative disaster: they are called just like Objective-C instance methods!

Here's our example:

#import <Foundation/Foundation.h>

void callClassMethod() {
  [NSString alloc];
}
_callClassMethod:                       ; @callClassMethod
        .cfi_startproc
; BB#0:
Lloh0:
        adrp    x8, L_OBJC_CLASSLIST_REFERENCES_$_@PAGE
Lloh1:
        ldr     x0, [x8, L_OBJC_CLASSLIST_REFERENCES_$_@PAGEOFF]
Lloh2:
        adrp    x8, L_OBJC_SELECTOR_REFERENCES_@PAGE
Lloh3:
        ldr     x1, [x8, L_OBJC_SELECTOR_REFERENCES_@PAGEOFF]
        b       _objc_msgSend
        .loh AdrpAdrp   Lloh0, Lloh2
        .loh AdrpLdr    Lloh0, Lloh1
        .loh AdrpLdr    Lloh2, Lloh3
        .cfi_endproc

        .section        __DATA,__objc_classrefs,regular,no_dead_strip
        .p2align        3               ; @"OBJC_CLASSLIST_REFERENCES_$_"
L_OBJC_CLASSLIST_REFERENCES_$_:
        .quad   _OBJC_CLASS_$_NSString

        .section        __TEXT,__objc_methname,cstring_literals
L_OBJC_METH_VAR_NAME_:                  ; @OBJC_METH_VAR_NAME_
        .asciz  "alloc"

        .section        __DATA,__objc_selrefs,literal_pointers,no_dead_strip
        .p2align        3               ; @OBJC_SELECTOR_REFERENCES_
L_OBJC_SELECTOR_REFERENCES_:
        .quad   L_OBJC_METH_VAR_NAME_

As we can see, the overhead is even higher than for instance methods: [MyClass doStuff] compiles to objc_msgSend(OBJC_CLASSLIST_REFERENCES[MYCLASS_OFFSET], OBJC_SELECTOR_REFERENCESS[DO_STUFF_OFFSET]); we have the same old load for the selector as well as a second load to fetch the global class pointer, and then we have objc_msgSend on top of that.

4.3 Dynamicity and optimization

Because the Objective-C runtime allows classes' method implementations to be changed at any time (see e.g., class_addMethod, method_setImplementation, dynamic method resolution, message forwarding, etc.), the compiler cannot possibly prove what code will actually be executed by any particular method call. This disables several common optimizations:

Common subexpression elimination
If you repeat a call to an inline getter method in C++, you can reasonably expect that that method will still be called only once. This is not the case in Objective-C, not even for properties. In other words, if you write if ([o foo]) { [o2 setBar:[o foo]]; }, -foo will get called twice. It does not matter if you spell it if (o.foo) { o2.bar = o.foo; }; the two are identical.
Inlining and devirtualization
Since the compiler cannot determine what method implementation will be executed, it certainly cannot inline that implementation or insert a direct call to it.

5 Blocks

Apple's documentation on the block ABI is quite thorough. Salient points:

  • Each instance of a particular block takes up at least 4 words on a 64-bit machine
  • Each block kind takes up another 2-5 words
  • Calling a block is implemented by calling a C function through a function pointer and passing it a pointer to the block instance

Let's just quickly verify it:

#include <stdio.h>
void callBlock(void (^block)(void)) {
  block();
}
_callBlock:                             ; @callBlock
; BB#0:
        ldr     x1, [x0, #16]
        br      x1

Sure looks like our block call is loading the invoke function pointer and calling it, just like we'd expect from Apple's documentation.

Access to captured variables is like struct access:

#include <Block.h>

typedef int (^intBlock)(int);

intBlock makeAdder(int x) {
  return Block_copy(^(int y) {
    return x + y;
  });
}

The assembly for the inner block is:

___makeAdder_block_invoke:              ; @__makeAdder_block_invoke
; BB#0:
        ldr     w8, [x0, #32]
        add             w0, w8, w1
        ret

6 Literals

Objective-C has convenient literal syntax for creating instances of NSString, NSNumber, NSArray, and NSDictionary. As explained in Clang's documentation, nearly all the literal syntax is just sugar for the appropriate constructor. The one exception is NSString:

#import <Foundation/Foundation.h>

NSString *getLiteral()
{
  return @"Hello";
}
        .section        __TEXT,__cstring,cstring_literals
L_.str:                                 ; @.str
        .asciz  "Hello"

        .section        __DATA,__cfstring
        .p2align        3               ; @_unnamed_cfstring_
L__unnamed_cfstring_:
        .quad   ___CFConstantStringClassReference
        .long   1992                    ; 0x7c8
        .space  4
        .quad   L_.str
        .quad   5                       ; 0x5

Our literal is stored as a structure with 4 elements: ___CFConstantStringClassReference (presumably the isa pointer), a constant that is probably used by the implementation, a pointer to the C-string version of the string, and its length. In other words, @"Hello" is not a shortcut for +[NSString stringWithUTF8String:"Hello"]; it's more efficient!

7 Conclusion

Because of the dynamic nature of Objective-C, using Objective-C features is often less efficient than C or C++ equivalents. On the other hand, Objective-C is easier to use than C or C++. Now that you know more about Objective-C performance, you can make better decisions about when to choose Objective-C.