From 40fa8e3f10526edeba9a33caeff5af1af91edfcd Mon Sep 17 00:00:00 2001 From: David Chisnall Date: Sat, 6 Jan 2024 11:30:32 +0000 Subject: [PATCH] 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 --- ANNOUNCE | 5 ++ CMakeLists.txt | 1 + Test/CMakeLists.txt | 2 + Test/DirectMethods.m | 45 +++++++++++++++ Test/FastPathAlloc.m | 129 +++++++++++++++++++++++++++++++++++++++++++ Test/Test.h | 1 + Test/Test.m | 5 ++ class.h | 11 +++- dtable.c | 65 +++++++++++++++++++++- fast_paths.m | 60 ++++++++++++++++++++ objc/runtime.h | 8 +++ 11 files changed, 329 insertions(+), 3 deletions(-) create mode 100644 Test/DirectMethods.m create mode 100644 Test/FastPathAlloc.m create mode 100644 fast_paths.m diff --git a/ANNOUNCE b/ANNOUNCE index 80c6964d..3f5aba2a 100644 --- a/ANNOUNCE +++ b/ANNOUNCE @@ -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: diff --git a/CMakeLists.txt b/CMakeLists.txt index 5009fd56..717a28b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,7 @@ set(libobjc_C_SRCS runtime.c sarray2.c sendmsg2.c + fast_paths.m ) set(libobjc_HDRS objc/Availability.h diff --git a/Test/CMakeLists.txt b/Test/CMakeLists.txt index 059fa820..fc190dfa 100644 --- a/Test/CMakeLists.txt +++ b/Test/CMakeLists.txt @@ -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 diff --git a/Test/DirectMethods.m b/Test/DirectMethods.m new file mode 100644 index 00000000..129959a1 --- /dev/null +++ b/Test/DirectMethods.m @@ -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 diff --git a/Test/FastPathAlloc.m b/Test/FastPathAlloc.m new file mode 100644 index 00000000..f09aedd4 --- /dev/null +++ b/Test/FastPathAlloc.m @@ -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 + +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 diff --git a/Test/Test.h b/Test/Test.h index e0e83719..04b12fb4 100644 --- a/Test/Test.h +++ b/Test/Test.h @@ -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; diff --git a/Test/Test.m b/Test/Test.m index 8d651a4b..c8c9b4a2 100644 --- a/Test/Test.m +++ b/Test/Test.m @@ -38,6 +38,10 @@ + (id)new { return class_createInstance(self, 0); } ++ (id)alloc +{ + return class_createInstance(self, 0); +} - (void)dealloc { object_dispose(self); @@ -55,6 +59,7 @@ - (void)release objc_release(self); } - (void)_ARCCompliantRetainRelease {} ++ (void)_TrivialAllocInit{} @end @implementation NSAutoreleasePool diff --git a/class.h b/class.h index 20e8cf9e..7a5b2404 100644 --- a/class.h +++ b/class.h @@ -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), }; /** diff --git a/dtable.c b/dtable.c index be928b77..54dc842b 100644 --- a/dtable.c +++ b/dtable.c @@ -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)) @@ -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 diff --git a/fast_paths.m b/fast_paths.m new file mode 100644 index 00000000..508df0eb --- /dev/null +++ b/fast_paths.m @@ -0,0 +1,60 @@ +#include "objc/runtime.h" +#include "class.h" + +typedef struct _NSZone NSZone; +@interface RootMethods +- (id)alloc; +- (id)allocWithZone: (NSZone*)aZone; +- (id)init; +@end +#include + +/** + * 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); + } + fprintf(stderr, "Testing metaclass %p (%s) for fast path: %d\n", cls->isa, class_getName(cls), objc_test_class_flag(cls->isa, objc_class_flag_fast_alloc_init)); + 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]; +} diff --git a/objc/runtime.h b/objc/runtime.h index 7a816ecd..ba9cf858 100644 --- a/objc/runtime.h +++ b/objc/runtime.h @@ -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 '#'