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
andl_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:
- 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.
- 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 itif (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.