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 '#'