Skip to content

Commit

Permalink
Add support for fast-path alloc / init methods and direct methods.
Browse files Browse the repository at this point in the history
The fast paths follow the pattern that we established for fast ARC:
Framework base classes can opt in by implementing a `+_TrivialAllocInit`
method.

This opt-in behaviour is inherited and is removed implicitly in any
subclass that implements alloc or init methods (alloc and init are
treated independently).

Compilers can emit calls to `objc_alloc(cls)` instead of `[cls alloc]`,
`objc_allocWithZone(cls)` instead of `[cls allocWithZone: NULL]`, and
`objc_alloc_init` instead of `[[cls alloc] init]`.

Direct methods don't require very much support in the runtime.  Apple
reuses their fast path for `-self` (which is supported only in the Apple
fork of clang, not the upstream version) for a fast init.  Given that
the first few fields of the runtime's class structure have been stable
for around 30 years, I'm happy moving the flags word (and the
initialised bit, in particular) into the public ABI.  This lets us do a
fast-path check for whether a class is initialised in class methods and
call `objc_send_initialize` if it isn't.  This function is now exposed
as part of the public ABI, it was there already and does the relevant
checks without invoking any of the message-sending machinery.

Fixes #165 #169
  • Loading branch information
davidchisnall committed Jan 7, 2024
1 parent 3c42c64 commit 40fa8e3
Show file tree
Hide file tree
Showing 11 changed files with 329 additions and 3 deletions.
5 changes: 5 additions & 0 deletions ANNOUNCE
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ Highlights of this release include:
maintainable.
- Several bug fixes in the ARC code, especially in corner cases surrounding
weak references.
- Support for fast-path allocation / initialisation functions. Root classes
that opt into this should implement `+_TrivialAllocInit` (this can be an
empty method, it is not called). Clang 18 or later will emit calls to the
fast-path functions for `+alloc`, `+allocWithZone:` and `+alloc` + `-init`
calls. This should improve code density as well as performance.

You may obtain the code for this release from git and use the 2.2 branch:

Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ set(libobjc_C_SRCS
runtime.c
sarray2.c
sendmsg2.c
fast_paths.m
)
set(libobjc_HDRS
objc/Availability.h
Expand Down
2 changes: 2 additions & 0 deletions Test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ set(TESTS
BlockTest_arc.m
ConstantString.m
Category.m
DirectMethods.m
ExceptionTest.m
FastARC.m
FastARCPool.m
FastPathAlloc.m
FastRefCount.m
Forward.m
ManyManySelectors.m
Expand Down
45 changes: 45 additions & 0 deletions Test/DirectMethods.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include "Test.h"

#if !__has_attribute(objc_direct)
int main()
{
return 77;
}
#else

static BOOL initializeCalled;
static BOOL directMethodCalled;

@interface HasDirect : Test
+ (void)clsDirect __attribute__((objc_direct));
- (int)instanceDirect __attribute__((objc_direct));
@end
@implementation HasDirect
+ (void)initialize
{
initializeCalled = YES;
}
+ (void)clsDirect
{
directMethodCalled = YES;
}
- (int)instanceDirect
{
return 42;
}
@end

int main(void)
{
[HasDirect clsDirect];
assert(directMethodCalled);
assert(initializeCalled);
HasDirect *obj = [HasDirect new];
assert([obj instanceDirect] == 42);
obj = nil;
assert([obj instanceDirect] == 0);
return 0;
}


#endif
129 changes: 129 additions & 0 deletions Test/FastPathAlloc.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#if __clang_major__ < 18
// Skip this test if clang is too old to support it.
int main(void)
{
return 77;
}
#else
#include "Test.h"
#include <stdio.h>

static BOOL called;

typedef struct _NSZone NSZone;

@interface ShouldAlloc : Test @end
@interface ShouldAllocWithZone : Test @end
@interface ShouldInit : Test @end
@interface ShouldInit2 : Test @end

@interface NoAlloc : Test @end
@interface NoInit : Test @end
@interface NoInit2 : NoInit @end

@implementation ShouldAlloc
+ (instancetype)alloc
{
called = YES;
fprintf(stderr, "[%s %s] called\n", class_getName(object_getClass(self)), sel_getName(_cmd));
return [super alloc];
}
@end
@implementation ShouldAllocWithZone
+ (instancetype)allocWithZone: (NSZone*)aZone
{
called = YES;
fprintf(stderr, "[%s %s] called\n", class_getName(object_getClass(self)), sel_getName(_cmd));
return [super alloc];
}
@end
@implementation ShouldInit
- (instancetype)init
{
called = YES;
fprintf(stderr, "[%s %s] called\n", class_getName(object_getClass(self)), sel_getName(_cmd));
return self;
}
@end
@implementation ShouldInit2
+ (instancetype)alloc
{
fprintf(stderr, "[%s %s] called\n", class_getName(object_getClass(self)), sel_getName(_cmd));
return [super alloc];
}
- (instancetype)init
{
called = YES;
fprintf(stderr, "[%s %s] called\n", class_getName(object_getClass(self)), sel_getName(_cmd));
return self;
}
@end

@implementation NoAlloc
+ (void)_TrivialAllocInit{}
+ (instancetype)alloc
{
called = YES;
fprintf(stderr, "[%s %s] called\n", class_getName(object_getClass(self)), sel_getName(_cmd));
return [super alloc];
}
+ (instancetype)allocWithZone: (NSZone*)aZone
{
called = YES;
fprintf(stderr, "[%s %s] called\n", class_getName(object_getClass(self)), sel_getName(_cmd));
return [super alloc];
}
@end
@implementation NoInit
+ (void)_TrivialAllocInit{}
- (instancetype)init
{
called = YES;
fprintf(stderr, "[%s %s] called\n", class_getName(object_getClass(self)), sel_getName(_cmd));
return self;
}
@end
@implementation NoInit2
+ (instancetype)alloc
{
fprintf(stderr, "[%s %s] called\n", class_getName(object_getClass(self)), sel_getName(_cmd));
return [super alloc];
}
@end

int main(void)
{
called = NO;
[ShouldAlloc alloc];
assert(called);

[ShouldAllocWithZone allocWithZone: NULL];
assert(called);
called = NO;

called = NO;
[[ShouldInit alloc] init];
assert(called);

called = NO;
[[ShouldInit2 alloc] init];
assert(called);

called = NO;
[NoAlloc alloc];
assert(!called);

[NoAlloc allocWithZone: NULL];
assert(!called);
called = NO;

called = NO;
[[NoInit alloc] init];
assert(!called);

called = NO;
[[NoInit2 alloc] init];
assert(!called);
}

#endif
1 change: 1 addition & 0 deletions Test/Test.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ __attribute__((objc_root_class))
@interface Test { id isa; }
+ (Class)class;
+ (id)new;
+ (id)alloc;
#if !__has_feature(objc_arc)
- (void)dealloc;
- (id)autorelease;
Expand Down
5 changes: 5 additions & 0 deletions Test/Test.m
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ + (id)new
{
return class_createInstance(self, 0);
}
+ (id)alloc
{
return class_createInstance(self, 0);
}
- (void)dealloc
{
object_dispose(self);
Expand All @@ -55,6 +59,7 @@ - (void)release
objc_release(self);
}
- (void)_ARCCompliantRetainRelease {}
+ (void)_TrivialAllocInit{}
@end

@implementation NSAutoreleasePool
Expand Down
11 changes: 10 additions & 1 deletion class.h
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,8 @@ enum objc_class_flags
* This class has been sent a +initalize message. This message is sent
* exactly once to every class that is sent a message by the runtime, just
* before the first other message is sent.
*
* For direct method support, this is now part of the public ABI.
*/
objc_class_flag_initialized = (1<<8),
/**
Expand Down Expand Up @@ -356,7 +358,14 @@ enum objc_class_flags
* safe to store directly into weak variables and to skip all reference
* count manipulations.
*/
objc_class_flag_permanent_instances = (1<<14)
objc_class_flag_permanent_instances = (1<<14),
/**
* On a metaclass, guarantees that `+alloc` and `+allocWithZone:` are
* trivial wrappers around `class_createInstance`.
*
* On a class, guarantees that `+init` is trivial.
*/
objc_class_flag_fast_alloc_init = (1<<15),
};

/**
Expand Down
65 changes: 63 additions & 2 deletions dtable.c
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,80 @@ static BOOL ownsMethod(Class cls, SEL sel)
#define ARC_DEBUG_LOG(...) do {} while(0)
#endif

/**
* Check whether this class pair implement or override `+alloc`,
* `+allocWithZone`, or `-init` in a way that requires the methods to be
* called.
*/
static void checkFastAllocInit(Class cls)
{
// This needs to be called on the class, not the metaclass
if (class_isMetaClass(cls))
{
fprintf(stderr, "%s called with metaclass, not class\n", class_getName(cls));
return;
}
static SEL alloc, allocWithZone, init, isTrivialAllocInit;
if (NULL == alloc)
{
alloc = sel_registerName("alloc");
allocWithZone = sel_registerName("allocWithZone:");
init = sel_registerName("init");
isTrivialAllocInit = sel_registerName("_TrivialAllocInit");
}
Class metaclass = cls->isa;
Class isTrivialOwner = ownerForMethod(metaclass, isTrivialAllocInit);
// If nothing in this hierarchy opts in to trivial alloc / init behaviour, give up.
if (isTrivialOwner == nil)
{
objc_clear_class_flag(cls, objc_class_flag_fast_alloc_init);
objc_clear_class_flag(metaclass, objc_class_flag_fast_alloc_init);
return;
}
// Check for overrides of alloc or allocWithZone:.
// This check has some false negatives. If you override only one of alloc
// or allocWithZone, both will hit the slow path. That's fine because the
// fast path is an optimisation, not a guarantee.
Class allocOwner = ownerForMethod(metaclass, alloc);
Class allocWithZoneOwner = ownerForMethod(metaclass, allocWithZone);
if (((allocOwner == nil) || (allocOwner == isTrivialOwner)) &&
((allocWithZoneOwner == nil) || (allocWithZoneOwner == isTrivialOwner)))
{
objc_set_class_flag(metaclass, objc_class_flag_fast_alloc_init);
}
else
{
objc_clear_class_flag(metaclass, objc_class_flag_fast_alloc_init);
}
Class initOwner = ownerForMethod(cls, init);
if ((initOwner == nil) || (initOwner->isa == isTrivialOwner))
{
objc_set_class_flag(cls, objc_class_flag_fast_alloc_init);
}
else
{
objc_clear_class_flag(cls, objc_class_flag_fast_alloc_init);
}
}

/**
* Checks whether the class implements memory management methods, and whether
* they are safe to use with ARC.
*/
static void checkARCAccessors(Class cls)
{
static SEL retain, release, autorelease, isARC;
checkFastAllocInit(cls);
static SEL retain, release, autorelease, isARC, alloc, allocWithZone, init, isTrivialAllocInit;
if (NULL == retain)
{
retain = sel_registerName("retain");
release = sel_registerName("release");
autorelease = sel_registerName("autorelease");
isARC = sel_registerName("_ARCCompliantRetainRelease");
alloc = sel_registerName("alloc");
allocWithZone = sel_registerName("allocWithZone:");
init = sel_registerName("init");
isTrivialAllocInit = sel_registerName("_TrivialAllocInit");
}
Class owner = ownerForMethod(cls, retain);
if ((NULL != owner) && !ownsMethod(owner, isARC))
Expand Down Expand Up @@ -661,7 +722,7 @@ static void remove_dtable(InitializingDtable* meta_buffer)
/**
* Send a +initialize message to the receiver, if required.
*/
PRIVATE void objc_send_initialize(id object)
OBJC_PUBLIC void objc_send_initialize(id object)
{
Class class = classForObject(object);
// If the first message is sent to an instance (weird, but possible and
Expand Down

0 comments on commit 40fa8e3

Please sign in to comment.