https://github.com/DataCorrupted updated https://github.com/llvm/llvm-project/pull/170619
>From bbf2e85a9bc07a52c83d13af5db0d35878484b9a Mon Sep 17 00:00:00 2001 From: Peter Rong <[email protected]> Date: Wed, 3 Dec 2025 22:45:04 -0800 Subject: [PATCH 1/9] [ExposeObjCDirect] Optimizations In many cases we can infer that class object has been realized --- clang/lib/CodeGen/CGObjCRuntime.cpp | 65 ++++++++++++++++++++++++++++- clang/lib/CodeGen/CGObjCRuntime.h | 23 +++++++--- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/clang/lib/CodeGen/CGObjCRuntime.cpp b/clang/lib/CodeGen/CGObjCRuntime.cpp index a4b4460fdc49c..fd227d9645ac1 100644 --- a/clang/lib/CodeGen/CGObjCRuntime.cpp +++ b/clang/lib/CodeGen/CGObjCRuntime.cpp @@ -415,7 +415,70 @@ bool CGObjCRuntime::canMessageReceiverBeNull( bool CGObjCRuntime::canClassObjectBeUnrealized( const ObjCInterfaceDecl *CalleeClassDecl, CodeGenFunction &CGF) const { - // TODO + if (!CalleeClassDecl) + return true; + + // Heuristic 1: +load method on this class + // If the class has a +load method, it's realized when the binary is loaded. + ASTContext &Ctx = CGM.getContext(); + const IdentifierInfo *LoadII = &Ctx.Idents.get("load"); + Selector LoadSel = Ctx.Selectors.getSelector(0, &LoadII); + + // TODO: if one if the child had +load, this class is guaranteed to be + // realized as well. We should have a translation unit specific map that + // precomputes all classes that are realized, and just do a lookup here. + // But we need to measure how expensive it is to create a map like that. + if (CalleeClassDecl->lookupClassMethod(LoadSel)) + return false; // This class has +load, so it's already realized + + // Heuristic 2: using Self / Super + // If we're currently executing a method of ClassDecl (or a subclass), + // then ClassDecl must already be realized. + if (const auto *CurMethod = + dyn_cast_or_null<ObjCMethodDecl>(CGF.CurCodeDecl)) { + const ObjCInterfaceDecl *CallerCalssDecl = CurMethod->getClassInterface(); + if (CallerCalssDecl && CalleeClassDecl->isSuperClassOf(CallerCalssDecl)) + return false; + } + + // Heuristic 3: previously realized + // Heuristic 3.1: Walk through the current BasicBlock looking for calls that + // realize the class. All heuristics in this cluster share the same + // implementation pattern. + auto *BB = CGF.Builder.GetInsertBlock(); + if (!BB) + return true; // No current block, assume unrealized + + llvm::StringRef CalleeClassName = CalleeClassDecl->getName(); + + // Heuristic 3.2 / TODO: If realization happened in a dominating block, the + // class is realized Requires Dominator tree analysis. There should be an + // outer loop `for (BB: DominatingBasicBlocks)` + for (const auto &Inst : *BB) { + // Check if this is a call instruction + const auto *Call = llvm::dyn_cast<llvm::CallInst>(&Inst); + if (!Call) + continue; + llvm::Function *CalledFunc = Call->getCalledFunction(); + if (!CalledFunc) + continue; + + llvm::StringRef FuncNamePtr = CalledFunc->getName(); + // Skip the \01 prefix if present + if (FuncNamePtr.starts_with("\01")) + FuncNamePtr = FuncNamePtr.drop_front(1); + // Check for instance method calls: "-[ClassName methodName]" + // or class method calls: "+[ClassName methodName]" + // Also check for thunks: "-[ClassName methodName]_thunk" + if ((FuncNamePtr.starts_with("-[") || FuncNamePtr.starts_with("+["))) { + FuncNamePtr = FuncNamePtr.drop_front(2); + // TODO: if the current class is the super class of the function that's + // used, it should've been realized as well + if (FuncNamePtr.starts_with(CalleeClassName)) + return false; + } + } + // Otherwise, assume it can be unrealized. return true; } diff --git a/clang/lib/CodeGen/CGObjCRuntime.h b/clang/lib/CodeGen/CGObjCRuntime.h index d3d4745cb77a7..b0cf04fc8553b 100644 --- a/clang/lib/CodeGen/CGObjCRuntime.h +++ b/clang/lib/CodeGen/CGObjCRuntime.h @@ -226,7 +226,7 @@ class CGObjCRuntime { virtual llvm::Function *GenerateMethod(const ObjCMethodDecl *OMD, const ObjCContainerDecl *CD) = 0; -/// Generates precondition checks for direct Objective-C Methods. + /// Generates precondition checks for direct Objective-C Methods. /// This includes [self self] for class methods and nil checks. virtual void GenerateDirectMethodsPreconditionCheck( CodeGenFunction &CGF, llvm::Function *Fn, const ObjCMethodDecl *OMD, @@ -330,10 +330,23 @@ class CGObjCRuntime { QualType resultType, CallArgList &callArgs); - bool canMessageReceiverBeNull(CodeGenFunction &CGF, - const ObjCMethodDecl *method, bool isSuper, - const ObjCInterfaceDecl *classReceiver, - llvm::Value *receiver); + /// Check if the receiver of an ObjC message send can be null. + /// Returns true if the receiver may be null, false if provably non-null. + /// + /// This can be overridden by subclasses to add runtime-specific heuristics. + /// Base implementation checks: + /// - Super dispatch (always non-null) + /// - Self in const-qualified methods (ARC) + /// - Weak-linked classes + /// + /// Future enhancements in CGObjCCommonMac override: + /// - _Nonnull attributes + /// - Results of alloc, new, ObjC literals + virtual bool canMessageReceiverBeNull(CodeGenFunction &CGF, + const ObjCMethodDecl *method, + bool isSuper, + const ObjCInterfaceDecl *classReceiver, + llvm::Value *receiver); /// Check if a class object can be unrealized (not yet initialized). /// Returns true if the class may be unrealized, false if provably realized. >From 49e0b9b6ac187a086244d8e48d51c1d57445043c Mon Sep 17 00:00:00 2001 From: Peter Rong <[email protected]> Date: Thu, 4 Dec 2025 11:34:27 -0800 Subject: [PATCH 2/9] update test and fix incorrect heuristic --- clang/lib/CodeGen/CGObjCRuntime.cpp | 47 ++---- ...pose-direct-method-opt-class-realization.m | 148 ++++++++++++++++++ clang/test/CodeGenObjC/expose-direct-method.m | 2 + 3 files changed, 160 insertions(+), 37 deletions(-) create mode 100644 clang/test/CodeGenObjC/expose-direct-method-opt-class-realization.m diff --git a/clang/lib/CodeGen/CGObjCRuntime.cpp b/clang/lib/CodeGen/CGObjCRuntime.cpp index fd227d9645ac1..f20ac144bc7ef 100644 --- a/clang/lib/CodeGen/CGObjCRuntime.cpp +++ b/clang/lib/CodeGen/CGObjCRuntime.cpp @@ -441,43 +441,16 @@ bool CGObjCRuntime::canClassObjectBeUnrealized( return false; } - // Heuristic 3: previously realized - // Heuristic 3.1: Walk through the current BasicBlock looking for calls that - // realize the class. All heuristics in this cluster share the same - // implementation pattern. - auto *BB = CGF.Builder.GetInsertBlock(); - if (!BB) - return true; // No current block, assume unrealized - - llvm::StringRef CalleeClassName = CalleeClassDecl->getName(); - - // Heuristic 3.2 / TODO: If realization happened in a dominating block, the - // class is realized Requires Dominator tree analysis. There should be an - // outer loop `for (BB: DominatingBasicBlocks)` - for (const auto &Inst : *BB) { - // Check if this is a call instruction - const auto *Call = llvm::dyn_cast<llvm::CallInst>(&Inst); - if (!Call) - continue; - llvm::Function *CalledFunc = Call->getCalledFunction(); - if (!CalledFunc) - continue; - - llvm::StringRef FuncNamePtr = CalledFunc->getName(); - // Skip the \01 prefix if present - if (FuncNamePtr.starts_with("\01")) - FuncNamePtr = FuncNamePtr.drop_front(1); - // Check for instance method calls: "-[ClassName methodName]" - // or class method calls: "+[ClassName methodName]" - // Also check for thunks: "-[ClassName methodName]_thunk" - if ((FuncNamePtr.starts_with("-[") || FuncNamePtr.starts_with("+["))) { - FuncNamePtr = FuncNamePtr.drop_front(2); - // TODO: if the current class is the super class of the function that's - // used, it should've been realized as well - if (FuncNamePtr.starts_with(CalleeClassName)) - return false; - } - } + // TODO: Heuristic 3: previously realized + // Walk through previous instructions can be inefficient, since + // `canClassObjectBeUnrealized` is called everytime we emit a class method. + // Besides, a realized subclass means parent class is realized. Therefore, + // a code like below also requires some special handling. + // + // ``` + // +[Child foo]; + // +[Parent foo]; + // ``` // Otherwise, assume it can be unrealized. return true; diff --git a/clang/test/CodeGenObjC/expose-direct-method-opt-class-realization.m b/clang/test/CodeGenObjC/expose-direct-method-opt-class-realization.m new file mode 100644 index 0000000000000..34f8537d75564 --- /dev/null +++ b/clang/test/CodeGenObjC/expose-direct-method-opt-class-realization.m @@ -0,0 +1,148 @@ +// RUN: %clang_cc1 -emit-llvm -fobjc-arc -triple arm64-apple-darwin10 \ +// RUN: -fobjc-expose-direct-methods %s -o - | FileCheck %s + +// ============================================================================ +// HEURISTIC 1: Classes with +load method skip thunk for class methods +// because they are guaranteed to be realized when the binary is loaded. +// ============================================================================ + +__attribute__((objc_root_class)) +@interface ClassWithLoad ++ (void)load; ++ (int)classDirectMethod __attribute__((objc_direct)); +@end + +@implementation ClassWithLoad + ++ (void)load { + // This method causes the class to be realized at load time +} + +// CHECK-LABEL: define hidden i32 @"+[ClassWithLoad classDirectMethod]"(ptr noundef %self) ++ (int)classDirectMethod { + return 42; +} + +@end + +// A class without +load method for comparison +__attribute__((objc_root_class)) +@interface ClassWithoutLoad ++ (int)classDirectMethod __attribute__((objc_direct)); +@end + +@implementation ClassWithoutLoad + +// CHECK-LABEL: define hidden i32 @"+[ClassWithoutLoad classDirectMethod]"(ptr noundef %self) ++ (int)classDirectMethod { + return 42; +} + +@end + +// CHECK-LABEL: define{{.*}} i32 @testClassWithLoad() +int testClassWithLoad(void) { + // Because ClassWithLoad has +load, it's guaranteed to be realized. + // So we should call the implementation directly, NOT through a thunk. + // + // CHECK: call i32 @"+[ClassWithLoad classDirectMethod]"(ptr noundef + // CHECK-NOT: call i32 @"+[ClassWithLoad classDirectMethod]_thunk" + return [ClassWithLoad classDirectMethod]; +} + +// CHECK-LABEL: define{{.*}} i32 @testClassWithoutLoad() +int testClassWithoutLoad(void) { + // ClassWithoutLoad has no +load, so the class might not be realized. + // We need to call through the thunk which will realize the class. + // + // CHECK: call i32 @"+[ClassWithoutLoad classDirectMethod]_thunk"(ptr noundef + return [ClassWithoutLoad classDirectMethod]; +} + +// ============================================================================ +// HEURISTIC 2: Calls from within the same class skip thunk +// because if we're executing a method of the class, it must be realized. +// ============================================================================ + +__attribute__((objc_root_class)) +@interface SameClassTest ++ (int)classDirectMethod __attribute__((objc_direct)); ++ (int)callerClassMethod __attribute__((objc_direct)); +- (int)callerInstanceMethod __attribute__((objc_direct)); +@end + +@implementation SameClassTest + +// CHECK-LABEL: define hidden i32 @"+[SameClassTest classDirectMethod]"(ptr noundef %self) ++ (int)classDirectMethod { + return 42; +} + +// CHECK-LABEL: define hidden i32 @"+[SameClassTest callerClassMethod]"(ptr noundef %self) ++ (int)callerClassMethod { + // Calling a class method from another class method of the SAME class. + // The class must be realized (we're already executing a method of it). + // Should call implementation directly, NOT through thunk. + // + // CHECK: call i32 @"+[SameClassTest classDirectMethod]"(ptr noundef + // CHECK-NOT: call i32 @"+[SameClassTest classDirectMethod]_thunk" + return [SameClassTest classDirectMethod]; +} + +// CHECK-LABEL: define hidden i32 @"-[SameClassTest callerInstanceMethod]"(ptr noundef %self) +- (int)callerInstanceMethod { + // Calling a class method from an instance method of the SAME class. + // The class must be realized (we're already executing a method of it). + // Should call implementation directly, NOT through thunk. + // + // CHECK: call i32 @"+[SameClassTest classDirectMethod]"(ptr noundef + // CHECK-NOT: call i32 @"+[SameClassTest classDirectMethod]_thunk" + return [SameClassTest classDirectMethod]; +} + +@end + +__attribute__((objc_root_class)) +@interface SuperClass ++ (int)superClassMethod __attribute__((objc_direct)); +@end + +@implementation SuperClass + +// CHECK-LABEL: define hidden i32 @"+[SuperClass superClassMethod]"(ptr noundef %self) ++ (int)superClassMethod { + return 100; +} + +@end + +@interface SubClass : SuperClass ++ (int)subCallerMethod __attribute__((objc_direct)); +- (int)subInstanceCaller __attribute__((objc_direct)); +@end + +@implementation SubClass + +// CHECK-LABEL: define hidden i32 @"+[SubClass subCallerMethod]"(ptr noundef %self) ++ (int)subCallerMethod { + // Calling a superclass's class method from a subclass method. + // SuperClass must be realized because SubClass inherits from it. + // Should call implementation directly, NOT through thunk. + // + // CHECK: call i32 @"+[SuperClass superClassMethod]"(ptr noundef + // CHECK-NOT: call i32 @"+[SuperClass superClassMethod]_thunk" + return [SuperClass superClassMethod]; +} + +// CHECK-LABEL: define hidden i32 @"-[SubClass subInstanceCaller]"(ptr noundef %self) +- (int)subInstanceCaller { + // Calling a superclass's class method from a subclass instance method. + // SuperClass must be realized because SubClass inherits from it. + // Should call implementation directly, NOT through thunk. + // + // CHECK: call i32 @"+[SuperClass superClassMethod]"(ptr noundef + // CHECK-NOT: call i32 @"+[SuperClass superClassMethod]_thunk" + return [SuperClass superClassMethod]; +} + +@end diff --git a/clang/test/CodeGenObjC/expose-direct-method.m b/clang/test/CodeGenObjC/expose-direct-method.m index 3d1420619774b..b57670763eae0 100644 --- a/clang/test/CodeGenObjC/expose-direct-method.m +++ b/clang/test/CodeGenObjC/expose-direct-method.m @@ -267,8 +267,10 @@ int useSRet(Root *r) { // CHECK: call void @"-[Root getAggregate]_thunk" [r getAggregate].a + // TODO: The compiler is not smart enough to know the class object must be realized yet. + // CHECK-NOT: call i64 @"+[Root classGetComplex]"(ptr noundef // CHECK: call i64 @"+[Root classGetComplex]_thunk"(ptr noundef [Root classGetComplex].a + + // CHECK-NOT: call void @"+[Root classGetAggregate]"(ptr {{.*}}sret // CHECK: call void @"+[Root classGetAggregate]_thunk"(ptr {{.*}}sret [Root classGetAggregate].a ); >From 0c89c244d66c1a0c36323bfb217162c0f0034c0c Mon Sep 17 00:00:00 2001 From: Peter Rong <[email protected]> Date: Thu, 4 Dec 2025 11:53:38 -0800 Subject: [PATCH 3/9] fix mac tests --- clang/lib/CodeGen/CGObjCRuntime.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/clang/lib/CodeGen/CGObjCRuntime.cpp b/clang/lib/CodeGen/CGObjCRuntime.cpp index f20ac144bc7ef..f336d0d3b9454 100644 --- a/clang/lib/CodeGen/CGObjCRuntime.cpp +++ b/clang/lib/CodeGen/CGObjCRuntime.cpp @@ -425,11 +425,14 @@ bool CGObjCRuntime::canClassObjectBeUnrealized( Selector LoadSel = Ctx.Selectors.getSelector(0, &LoadII); // TODO: if one if the child had +load, this class is guaranteed to be - // realized as well. We should have a translation unit specific map that - // precomputes all classes that are realized, and just do a lookup here. - // But we need to measure how expensive it is to create a map like that. - if (CalleeClassDecl->lookupClassMethod(LoadSel)) - return false; // This class has +load, so it's already realized + // realized as well. We can't search for all child classes here. Ideally, we + // should have a translation unit level `SmallSet` to include all classes with + // +load. Every time a class has +load, put itself and all parents in it, and + // we can just query that `SmallSet` here. + if (CalleeClassDecl->lookupMethod(LoadSel, /*isInstance=*/false, + /*shallowCategoryLookup=*/false, + /*followSuper=*/false)) + return false; // Heuristic 2: using Self / Super // If we're currently executing a method of ClassDecl (or a subclass), >From 7aa65a2f74b47af04223e20602b716ddd5609767 Mon Sep 17 00:00:00 2001 From: Peter Rong <[email protected]> Date: Thu, 4 Dec 2025 13:12:20 -0800 Subject: [PATCH 4/9] evict weak class --- clang/lib/CodeGen/CGObjCRuntime.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clang/lib/CodeGen/CGObjCRuntime.cpp b/clang/lib/CodeGen/CGObjCRuntime.cpp index f336d0d3b9454..0e41c9c6b9105 100644 --- a/clang/lib/CodeGen/CGObjCRuntime.cpp +++ b/clang/lib/CodeGen/CGObjCRuntime.cpp @@ -415,7 +415,7 @@ bool CGObjCRuntime::canMessageReceiverBeNull( bool CGObjCRuntime::canClassObjectBeUnrealized( const ObjCInterfaceDecl *CalleeClassDecl, CodeGenFunction &CGF) const { - if (!CalleeClassDecl) + if (!CalleeClassDecl || isWeakLinkedClass(CalleeClassDecl)) return true; // Heuristic 1: +load method on this class >From 97bef2a53d288399177f2effcd0a152f516ba341 Mon Sep 17 00:00:00 2001 From: Peter Rong <[email protected]> Date: Thu, 4 Dec 2025 15:36:54 -0800 Subject: [PATCH 5/9] fix some lint warnings --- clang/lib/CodeGen/CGObjCRuntime.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/clang/lib/CodeGen/CGObjCRuntime.cpp b/clang/lib/CodeGen/CGObjCRuntime.cpp index 0e41c9c6b9105..88373fd0426b2 100644 --- a/clang/lib/CodeGen/CGObjCRuntime.cpp +++ b/clang/lib/CodeGen/CGObjCRuntime.cpp @@ -397,10 +397,10 @@ bool CGObjCRuntime::canMessageReceiverBeNull( // If we're emitting a method, and self is const (meaning just ARC, for now), // and the receiver is a load of self, then self is a valid object. - if (auto curMethod = dyn_cast_or_null<ObjCMethodDecl>(CGF.CurCodeDecl)) { - auto self = curMethod->getSelfDecl(); + if (const auto *curMethod = dyn_cast_or_null<ObjCMethodDecl>(CGF.CurCodeDecl)) { + const auto *self = curMethod->getSelfDecl(); if (self->getType().isConstQualified()) { - if (auto LI = dyn_cast<llvm::LoadInst>(receiver->stripPointerCasts())) { + if (const auto *LI = dyn_cast<llvm::LoadInst>(receiver->stripPointerCasts())) { llvm::Value *selfAddr = CGF.GetAddrOfLocalVar(self).emitRawPointer(CGF); if (selfAddr == LI->getPointerOperand()) { return false; >From 5cb282dd467fedfcc02ea26238c797fba658e213 Mon Sep 17 00:00:00 2001 From: Peter Rong <[email protected]> Date: Thu, 4 Dec 2025 15:42:20 -0800 Subject: [PATCH 6/9] format --- clang/lib/CodeGen/CGObjCRuntime.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/clang/lib/CodeGen/CGObjCRuntime.cpp b/clang/lib/CodeGen/CGObjCRuntime.cpp index 88373fd0426b2..be027777747d5 100644 --- a/clang/lib/CodeGen/CGObjCRuntime.cpp +++ b/clang/lib/CodeGen/CGObjCRuntime.cpp @@ -397,10 +397,12 @@ bool CGObjCRuntime::canMessageReceiverBeNull( // If we're emitting a method, and self is const (meaning just ARC, for now), // and the receiver is a load of self, then self is a valid object. - if (const auto *curMethod = dyn_cast_or_null<ObjCMethodDecl>(CGF.CurCodeDecl)) { + if (const auto *curMethod = + dyn_cast_or_null<ObjCMethodDecl>(CGF.CurCodeDecl)) { const auto *self = curMethod->getSelfDecl(); if (self->getType().isConstQualified()) { - if (const auto *LI = dyn_cast<llvm::LoadInst>(receiver->stripPointerCasts())) { + if (const auto *LI = + dyn_cast<llvm::LoadInst>(receiver->stripPointerCasts())) { llvm::Value *selfAddr = CGF.GetAddrOfLocalVar(self).emitRawPointer(CGF); if (selfAddr == LI->getPointerOperand()) { return false; >From 0ecb1b2af4554f3832859a4e48d2d559c9ad7145 Mon Sep 17 00:00:00 2001 From: Peter Rong <[email protected]> Date: Thu, 4 Dec 2025 15:53:24 -0800 Subject: [PATCH 7/9] simplify tests --- ...pose-direct-method-opt-class-realization.m | 84 ++++++++----------- 1 file changed, 33 insertions(+), 51 deletions(-) diff --git a/clang/test/CodeGenObjC/expose-direct-method-opt-class-realization.m b/clang/test/CodeGenObjC/expose-direct-method-opt-class-realization.m index 34f8537d75564..8814179ccb8e3 100644 --- a/clang/test/CodeGenObjC/expose-direct-method-opt-class-realization.m +++ b/clang/test/CodeGenObjC/expose-direct-method-opt-class-realization.m @@ -7,7 +7,18 @@ // ============================================================================ __attribute__((objc_root_class)) -@interface ClassWithLoad +@interface Root ++ (int)rootDirectMethod __attribute__((objc_direct)); +@end + +@implementation Root + +// CHECK-LABEL: define hidden i32 @"+[Root rootDirectMethod]"(ptr noundef %self) ++ (int)rootDirectMethod { return 100; } + +@end + +@interface ClassWithLoad : Root + (void)load; + (int)classDirectMethod __attribute__((objc_direct)); @end @@ -19,15 +30,12 @@ + (void)load { } // CHECK-LABEL: define hidden i32 @"+[ClassWithLoad classDirectMethod]"(ptr noundef %self) -+ (int)classDirectMethod { - return 42; -} ++ (int)classDirectMethod { return 42; } @end // A class without +load method for comparison -__attribute__((objc_root_class)) -@interface ClassWithoutLoad +@interface ClassWithoutLoad : Root + (int)classDirectMethod __attribute__((objc_direct)); @end @@ -64,8 +72,7 @@ int testClassWithoutLoad(void) { // because if we're executing a method of the class, it must be realized. // ============================================================================ -__attribute__((objc_root_class)) -@interface SameClassTest +@interface SameClassTest : Root + (int)classDirectMethod __attribute__((objc_direct)); + (int)callerClassMethod __attribute__((objc_direct)); - (int)callerInstanceMethod __attribute__((objc_direct)); @@ -86,7 +93,17 @@ + (int)callerClassMethod { // // CHECK: call i32 @"+[SameClassTest classDirectMethod]"(ptr noundef // CHECK-NOT: call i32 @"+[SameClassTest classDirectMethod]_thunk" - return [SameClassTest classDirectMethod]; + int a = [SameClassTest classDirectMethod]; + + // Calling the root class's class method from a subclass method. + // Root must be realized because SubClass inherits from it. + // Should call implementation directly, NOT through thunk. + // + // CHECK: call i32 @"+[Root rootDirectMethod]"(ptr noundef + // CHECK-NOT: call i32 @"+[Root rootDirectMethod]_thunk" + int b = [Root rootDirectMethod]; + + return a + b; } // CHECK-LABEL: define hidden i32 @"-[SameClassTest callerInstanceMethod]"(ptr noundef %self) @@ -97,52 +114,17 @@ - (int)callerInstanceMethod { // // CHECK: call i32 @"+[SameClassTest classDirectMethod]"(ptr noundef // CHECK-NOT: call i32 @"+[SameClassTest classDirectMethod]_thunk" - return [SameClassTest classDirectMethod]; -} + int a = [SameClassTest classDirectMethod]; -@end - -__attribute__((objc_root_class)) -@interface SuperClass -+ (int)superClassMethod __attribute__((objc_direct)); -@end - -@implementation SuperClass - -// CHECK-LABEL: define hidden i32 @"+[SuperClass superClassMethod]"(ptr noundef %self) -+ (int)superClassMethod { - return 100; -} - -@end - -@interface SubClass : SuperClass -+ (int)subCallerMethod __attribute__((objc_direct)); -- (int)subInstanceCaller __attribute__((objc_direct)); -@end - -@implementation SubClass - -// CHECK-LABEL: define hidden i32 @"+[SubClass subCallerMethod]"(ptr noundef %self) -+ (int)subCallerMethod { - // Calling a superclass's class method from a subclass method. - // SuperClass must be realized because SubClass inherits from it. + // Calling the root class's class method from a subclass instance method. + // Root must be realized because SubClass inherits from it. // Should call implementation directly, NOT through thunk. // - // CHECK: call i32 @"+[SuperClass superClassMethod]"(ptr noundef - // CHECK-NOT: call i32 @"+[SuperClass superClassMethod]_thunk" - return [SuperClass superClassMethod]; -} + // CHECK: call i32 @"+[Root rootDirectMethod]"(ptr noundef + // CHECK-NOT: call i32 @"+[Root rootDirectMethod]_thunk" + int b = [Root rootDirectMethod]; -// CHECK-LABEL: define hidden i32 @"-[SubClass subInstanceCaller]"(ptr noundef %self) -- (int)subInstanceCaller { - // Calling a superclass's class method from a subclass instance method. - // SuperClass must be realized because SubClass inherits from it. - // Should call implementation directly, NOT through thunk. - // - // CHECK: call i32 @"+[SuperClass superClassMethod]"(ptr noundef - // CHECK-NOT: call i32 @"+[SuperClass superClassMethod]_thunk" - return [SuperClass superClassMethod]; + return a + b; } @end >From bceeae80772504cece97691e5e1a1e97dbf07808 Mon Sep 17 00:00:00 2001 From: Peter Rong <[email protected]> Date: Thu, 4 Dec 2025 16:09:51 -0800 Subject: [PATCH 8/9] Add a cache to remember all classes that should've been realized by load --- clang/lib/CodeGen/CGObjCRuntime.cpp | 51 +++++++++++++++++++++-------- clang/lib/CodeGen/CGObjCRuntime.h | 10 ++++++ 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/clang/lib/CodeGen/CGObjCRuntime.cpp b/clang/lib/CodeGen/CGObjCRuntime.cpp index be027777747d5..0ed4feac31ea8 100644 --- a/clang/lib/CodeGen/CGObjCRuntime.cpp +++ b/clang/lib/CodeGen/CGObjCRuntime.cpp @@ -420,20 +420,11 @@ bool CGObjCRuntime::canClassObjectBeUnrealized( if (!CalleeClassDecl || isWeakLinkedClass(CalleeClassDecl)) return true; - // Heuristic 1: +load method on this class - // If the class has a +load method, it's realized when the binary is loaded. - ASTContext &Ctx = CGM.getContext(); - const IdentifierInfo *LoadII = &Ctx.Idents.get("load"); - Selector LoadSel = Ctx.Selectors.getSelector(0, &LoadII); - - // TODO: if one if the child had +load, this class is guaranteed to be - // realized as well. We can't search for all child classes here. Ideally, we - // should have a translation unit level `SmallSet` to include all classes with - // +load. Every time a class has +load, put itself and all parents in it, and - // we can just query that `SmallSet` here. - if (CalleeClassDecl->lookupMethod(LoadSel, /*isInstance=*/false, - /*shallowCategoryLookup=*/false, - /*followSuper=*/false)) + // Heuristic 1: +load method on this class or any subclass + // If the class or any of its subclasses has a +load method, it's realized + // when the binary is loaded. We cache this information to avoid repeatedly + // scanning the translation unit. + if (getOrPopulateRealizedClasses().contains(CalleeClassDecl)) return false; // Heuristic 2: using Self / Super @@ -461,6 +452,38 @@ bool CGObjCRuntime::canClassObjectBeUnrealized( return true; } +const RealizedClassSet &CGObjCRuntime::getOrPopulateRealizedClasses() const { + if (RealizedClasses) + return *RealizedClasses; + RealizedClasses = llvm::DenseSet<const ObjCInterfaceDecl *>(); + + ASTContext &Ctx = CGM.getContext(); + const IdentifierInfo *LoadII = &Ctx.Idents.get("load"); + Selector LoadSel = Ctx.Selectors.getSelector(0, &LoadII); + + TranslationUnitDecl *TUDecl = Ctx.getTranslationUnitDecl(); + llvm::DenseSet<const ObjCInterfaceDecl *> VisitedClasses; + for (const auto *D : TUDecl->decls()) { + if (const auto *OID = dyn_cast<ObjCInterfaceDecl>(D)) { + if (VisitedClasses.contains(OID)) + continue; + // Check if this class has a +load method + if (OID->lookupMethod(LoadSel, /*isInstance=*/false, + /*shallowCategoryLookup=*/false, + /*followSuper=*/false)) { + // Add this class and all its superclasses to the realized set + const ObjCInterfaceDecl *Cls = OID; + while (Cls) { + RealizedClasses->insert(Cls); + VisitedClasses.insert(Cls); + Cls = Cls->getSuperClass(); + } + } + } + } + return *RealizedClasses; +} + bool CGObjCRuntime::isWeakLinkedClass(const ObjCInterfaceDecl *ID) { do { if (ID->isWeakImported()) diff --git a/clang/lib/CodeGen/CGObjCRuntime.h b/clang/lib/CodeGen/CGObjCRuntime.h index b0cf04fc8553b..06faea476cc34 100644 --- a/clang/lib/CodeGen/CGObjCRuntime.h +++ b/clang/lib/CodeGen/CGObjCRuntime.h @@ -20,6 +20,7 @@ #include "CGValue.h" #include "clang/AST/DeclObjC.h" #include "clang/Basic/IdentifierTable.h" // Selector +#include "llvm/ADT/DenseSet.h" #include "llvm/ADT/UniqueVector.h" namespace llvm { @@ -60,6 +61,7 @@ class CGBlockInfo; // FIXME: Several methods should be pure virtual but aren't to avoid the // partially-implemented subclass breaking. +typedef llvm::DenseSet<const ObjCInterfaceDecl *> RealizedClassSet; /// Implements runtime-specific code generation functions. class CGObjCRuntime { @@ -67,6 +69,14 @@ class CGObjCRuntime { CodeGen::CodeGenModule &CGM; CGObjCRuntime(CodeGen::CodeGenModule &CGM) : CGM(CGM) {} + /// Cache of classes that are guaranteed to be realized because they or one + /// of their subclasses has a +load method. Lazily populated on first query. + mutable std::optional<RealizedClassSet> RealizedClasses; + + /// Populate the RealizedClasses cache by scanning all ObjCInterfaceDecls + /// in the translation unit for +load methods. + const RealizedClassSet &getOrPopulateRealizedClasses() const; + // Utility functions for unified ivar access. These need to // eventually be folded into other places (the structure layout // code). >From a8aa6a9374540d80dab497ef7a21f040d1a22130 Mon Sep 17 00:00:00 2001 From: Peter Rong <[email protected]> Date: Fri, 5 Dec 2025 11:02:06 -0800 Subject: [PATCH 9/9] Add a cache to remember previously realized classes --- clang/lib/CodeGen/CGObjCMac.cpp | 14 +++++++ clang/lib/CodeGen/CGObjCRuntime.cpp | 32 +++++++++++----- clang/lib/CodeGen/CodeGenFunction.h | 9 +++++ ...pose-direct-method-opt-class-realization.m | 37 +++++++++++++++++++ clang/test/CodeGenObjC/expose-direct-method.m | 7 ++-- 5 files changed, 86 insertions(+), 13 deletions(-) diff --git a/clang/lib/CodeGen/CGObjCMac.cpp b/clang/lib/CodeGen/CGObjCMac.cpp index 641302d7d32bc..5b60342d13758 100644 --- a/clang/lib/CodeGen/CGObjCMac.cpp +++ b/clang/lib/CodeGen/CGObjCMac.cpp @@ -2190,6 +2190,20 @@ CodeGen::RValue CGObjCCommonMac::EmitMessageSend( CallSite->setDoesNotReturn(); } + // If this was a class method call on a non-weakly-linked class, record it + // as realized for the "previously realized" heuristic. + if (ClassReceiver && Method && !isWeakLinkedClass(ClassReceiver)) { + if (llvm::BasicBlock *CurrentBB = CGF.Builder.GetInsertBlock()) + // 1. Class methods have forced class realization (regardless direct or + // not) + // 2. Direct methods whose receiver is not null means the class is + // previously realized. + if (Method->isClassMethod() || + (Method->isInstanceMethod() && !ReceiverCanBeNull)) { + CGF.ObjCRealizedClasses[CurrentBB].insert(ClassReceiver); + } + } + return nullReturn.complete(CGF, Return, rvalue, ResultType, CallArgs, RequiresNullCheck ? Method : nullptr); } diff --git a/clang/lib/CodeGen/CGObjCRuntime.cpp b/clang/lib/CodeGen/CGObjCRuntime.cpp index 0ed4feac31ea8..82780e3268f9e 100644 --- a/clang/lib/CodeGen/CGObjCRuntime.cpp +++ b/clang/lib/CodeGen/CGObjCRuntime.cpp @@ -437,16 +437,30 @@ bool CGObjCRuntime::canClassObjectBeUnrealized( return false; } - // TODO: Heuristic 3: previously realized - // Walk through previous instructions can be inefficient, since - // `canClassObjectBeUnrealized` is called everytime we emit a class method. - // Besides, a realized subclass means parent class is realized. Therefore, - // a code like below also requires some special handling. + // Heuristic 3: previously realized classes + // If we've already emitted a class method call for this class (or a subclass) + // earlier, then the class must be realized. // - // ``` - // +[Child foo]; - // +[Parent foo]; - // ``` + // TODO: Iter over all dominating blocks instead of just looking at the + // current block. While we can construct a DT using CFG.CurFn, it is expensive + // to do so repeatly when CGF is still emitting blocks. + if (auto *CurBB = CGF.Builder.GetInsertBlock()) { + auto It = CGF.ObjCRealizedClasses.find(CurBB); + if (It != CGF.ObjCRealizedClasses.end()) { + // Check if CalleeClassDecl is the same as or a superclass of any + // realized class in the cache. A realized subclass implies the parent + // is realized. + for (const auto *RealizedClass : It->second) { + if (CalleeClassDecl == RealizedClass) + return false; + if (CalleeClassDecl->isSuperClassOf(RealizedClass)) { + // Also cache this class to reduce future `isSuperClassOf` calls + It->second.insert(CalleeClassDecl); + return false; + } + } + } + } // Otherwise, assume it can be unrealized. return true; diff --git a/clang/lib/CodeGen/CodeGenFunction.h b/clang/lib/CodeGen/CodeGenFunction.h index f507146b37cc5..159d06022eda9 100644 --- a/clang/lib/CodeGen/CodeGenFunction.h +++ b/clang/lib/CodeGen/CodeGenFunction.h @@ -870,6 +870,15 @@ class CodeGenFunction : public CodeGenTypeCache { /// rethrows. SmallVector<llvm::Value *, 8> ObjCEHValueStack; + /// Per-basic-block cache of ObjC classes that have been realized during + /// codegen. When a class method is emitted on a non-weakly-linked class, + /// we record it here. This supports the "previously realized" heuristic + /// in canClassObjectBeUnrealized. The structure supports future + /// dominator-based analysis where we can check dominating blocks. + llvm::DenseMap<llvm::BasicBlock *, + llvm::SmallPtrSet<const ObjCInterfaceDecl *, 4>> + ObjCRealizedClasses; + /// A class controlling the emission of a finally block. class FinallyInfo { /// Where the catchall's edge through the cleanup should go. diff --git a/clang/test/CodeGenObjC/expose-direct-method-opt-class-realization.m b/clang/test/CodeGenObjC/expose-direct-method-opt-class-realization.m index 8814179ccb8e3..cf43123b3ab40 100644 --- a/clang/test/CodeGenObjC/expose-direct-method-opt-class-realization.m +++ b/clang/test/CodeGenObjC/expose-direct-method-opt-class-realization.m @@ -128,3 +128,40 @@ - (int)callerInstanceMethod { } @end + +// ============================================================================ +// HEURISTIC 3: Previously realized classes in the same basic block skip thunk. +// If we've already called a class method (which realizes the class), +// subsequent calls to the same class or its superclasses can skip the thunk. +// ============================================================================ + +// CHECK-LABEL: define{{.*}} i32 @testPreviouslyRealizedParentClass +int testPreviouslyRealizedParentClass(int flag) { + if (flag) { + // First call to ClassWithoutLoad - needs thunk (class might not be realized) + // CHECK: call i32 @"+[ClassWithoutLoad classDirectMethod]_thunk"(ptr noundef + int a = [ClassWithoutLoad classDirectMethod]; + + // Second call to same class - should skip thunk (class was just realized) + // CHECK: call i32 @"+[ClassWithoutLoad classDirectMethod]"(ptr noundef + // CHECK-NOT: call i32 @"+[ClassWithoutLoad classDirectMethod]_thunk" + int b = [ClassWithoutLoad classDirectMethod]; + + // Call to Root (parent of ClassWithoutLoad) - should skip thunk + // because realizing ClassWithoutLoad also realizes its superclass Root. + // CHECK: call i32 @"+[Root rootDirectMethod]"(ptr noundef + // CHECK-NOT: call i32 @"+[Root rootDirectMethod]_thunk" + int c = [Root rootDirectMethod]; + return a + b + c; + + } + // New block, we are not sure if prev block is executed, so we have to conservatively realize again. + // CHECK: call i32 @"+[ClassWithoutLoad classDirectMethod]_thunk" + // CHECK-NOT: call i32 @"+[ClassWithoutLoad classDirectMethod]"(ptr noundef + int b = [ClassWithoutLoad classDirectMethod]; + // CHECK: call i32 @"+[Root rootDirectMethod]"(ptr noundef + // CHECK-NOT: call i32 @"+[Root rootDirectMethod]_thunk" + int c = [Root rootDirectMethod]; + + return b + c; +} diff --git a/clang/test/CodeGenObjC/expose-direct-method.m b/clang/test/CodeGenObjC/expose-direct-method.m index b57670763eae0..fceedf4e944c0 100644 --- a/clang/test/CodeGenObjC/expose-direct-method.m +++ b/clang/test/CodeGenObjC/expose-direct-method.m @@ -266,12 +266,11 @@ int useSRet(Root *r) { // TODO: we should know that this instance is non nil. // CHECK: call void @"-[Root getAggregate]_thunk" [r getAggregate].a + - // TODO: The compiler is not smart enough to know the class object must be realized yet. // CHECK-NOT: call i64 @"+[Root classGetComplex]"(ptr noundef // CHECK: call i64 @"+[Root classGetComplex]_thunk"(ptr noundef [Root classGetComplex].a + - // CHECK-NOT: call void @"+[Root classGetAggregate]"(ptr {{.*}}sret - // CHECK: call void @"+[Root classGetAggregate]_thunk"(ptr {{.*}}sret + // CHECK-NOT: call void @"+[Root classGetAggregate]_thunk"(ptr {{.*}}sret + // CHECK: call void @"+[Root classGetAggregate]"(ptr {{.*}}sret [Root classGetAggregate].a ); } @@ -291,4 +290,4 @@ int useSRet(Root *r) { // CHECK: ret void // CHECK: define {{.*}} @"+[Root classGetComplex]_thunk" -// CHECK: define {{.*}} @"+[Root classGetAggregate]_thunk" +// CHECK-NOT: define {{.*}} @"+[Root classGetAggregate]_thunk" _______________________________________________ llvm-branch-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits
