Add support for fast-path alloc / init methods and direct methods.

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
main
David Chisnall 2 years ago committed by David Chisnall
parent 65280908eb
commit 377a81d237

@ -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:

@ -84,6 +84,7 @@ set(libobjc_C_SRCS
runtime.c
sarray2.c
sendmsg2.c
fast_paths.m
)
set(libobjc_HDRS
objc/Availability.h

@ -84,6 +84,8 @@ endif()
# shouldn't be run in legacy mode.
set(NEW_TESTS
category_properties.m
DirectMethods.m
FastPathAlloc.m
)
remove_definitions(-D__OBJC_RUNTIME_INTERNAL__=1)

@ -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

@ -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

@ -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;

@ -38,6 +38,10 @@
{
return class_createInstance(self, 0);
}
+ (id)alloc
{
return class_createInstance(self, 0);
}
- (void)dealloc
{
object_dispose(self);
@ -55,6 +59,7 @@
objc_release(self);
}
- (void)_ARCCompliantRetainRelease {}
+ (void)_TrivialAllocInit{}
@end
@implementation NSAutoreleasePool

@ -18,6 +18,20 @@
#import "objc/hooks.h"
#import "objc/objc-arc.h"
#import "objc/blocks_runtime.h"
#include "objc/message.h"
/**
* Helper to send a manual message for retain / release.
* We cannot use [object retain] and friends because recent clang will turn
* that into a call to `objc_retain`, causing infinite recursion.
*/
#ifdef __GNUSTEP_MSGSEND__
#define ManualRetainReleaseMessage(object, selName, types) \
((types)objc_msgSend)(object, @selector(selName))
#else
#define ManualRetainReleaseMessage(object, selName, types) \
((types)(objc_msg_lookup(object, @selector(selName))))(object, @selector(selName))
#endif
extern "C" id (*_objc_weak_load)(id object);
@ -310,7 +324,7 @@ static inline id retain(id obj, BOOL isWeak)
{
return retain_fast(obj, isWeak);
}
return [obj retain];
return ManualRetainReleaseMessage(obj, retain, id(*)(id, SEL));
}
extern "C" OBJC_PUBLIC BOOL objc_release_fast_no_destroy_np(id obj)
@ -376,7 +390,7 @@ static inline void release(id obj)
objc_release_fast_np(obj);
return;
}
[obj release];
return ManualRetainReleaseMessage(obj, release, void(*)(id, SEL));
}
static inline void initAutorelease(void)
@ -436,7 +450,7 @@ static inline id autorelease(id obj)
}
return obj;
}
return [obj autorelease];
return ManualRetainReleaseMessage(obj, autorelease, id(*)(id, SEL));
}
extern "C" OBJC_PUBLIC unsigned long objc_arc_autorelease_count_np(void)

@ -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),
/**
@ -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),
};
/**

@ -83,12 +83,68 @@ 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))
{
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)
{
checkFastAllocInit(cls);
static SEL retain, release, autorelease, isARC;
if (NULL == retain)
{
@ -661,7 +717,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

@ -0,0 +1,59 @@
#include "objc/runtime.h"
#include "class.h"
typedef struct _NSZone NSZone;
@interface RootMethods
- (id)alloc;
- (id)allocWithZone: (NSZone*)aZone;
- (id)init;
@end
#include <stdio.h>
/**
* Equivalent to [cls alloc]. If there's a fast path opt-in, then this skips the message send.
*/
id
objc_alloc(Class cls)
{
if (UNLIKELY(!objc_test_class_flag(cls->isa, objc_class_flag_initialized)))
{
objc_send_initialize(cls);
}
if (objc_test_class_flag(cls->isa, objc_class_flag_fast_alloc_init))
{
return class_createInstance(cls, 0);
}
return [cls alloc];
}
/**
* Equivalent to [cls allocWithZone: null]. If there's a fast path opt-in, then this skips the message send.
*/
id
objc_allocWithZone(Class cls)
{
if (UNLIKELY(!objc_test_class_flag(cls->isa, objc_class_flag_initialized)))
{
objc_send_initialize(cls);
}
if (objc_test_class_flag(cls->isa, objc_class_flag_fast_alloc_init))
{
return class_createInstance(cls, 0);
}
return [cls allocWithZone: NULL];
}
/**
* Equivalent to [[cls alloc] init]. If there's a fast path opt-in, then this
* skips the message send.
*/
id
objc_alloc_init(Class cls)
{
id instance = objc_alloc(cls);
if (objc_test_class_flag(cls, objc_class_flag_fast_alloc_init))
{
return instance;
}
return [instance init];
}

@ -1166,6 +1166,14 @@ int objc_set_apple_compatible_objcxx_exceptions(int newValue) OBJC_NONPORTABLE;
OBJC_PUBLIC
void __attribute__((weak)) objc_enumerationMutation(id obj);
/**
* Ensure that `+initialize` has been sent to the class of the argument (or the
* argument, if it is a class). This will not call `+initialize` if it has
* been called already, either via an explicit call to this function or by
* being sent some other message.
*/
OBJC_PUBLIC
void objc_send_initialize(id object) OBJC_NONPORTABLE;
#define _C_ID '@'
#define _C_CLASS '#'

Loading…
Cancel
Save