[llvm-branch-commits] [clang] [ExposeObjCDirect] Optimizations (PR #170619)
Peter Rong via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Fri Dec 5 11:02:51 PST 2025
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 <PeterRong at meta.com>
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 <PeterRong at meta.com>
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))
+ at interface ClassWithLoad
++ (void)load;
++ (int)classDirectMethod __attribute__((objc_direct));
+ at end
+
+ at 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;
+}
+
+ at end
+
+// A class without +load method for comparison
+__attribute__((objc_root_class))
+ at interface ClassWithoutLoad
++ (int)classDirectMethod __attribute__((objc_direct));
+ at end
+
+ at implementation ClassWithoutLoad
+
+// CHECK-LABEL: define hidden i32 @"+[ClassWithoutLoad classDirectMethod]"(ptr noundef %self)
++ (int)classDirectMethod {
+ return 42;
+}
+
+ at 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))
+ at interface SameClassTest
++ (int)classDirectMethod __attribute__((objc_direct));
++ (int)callerClassMethod __attribute__((objc_direct));
+- (int)callerInstanceMethod __attribute__((objc_direct));
+ at end
+
+ at 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];
+}
+
+ at end
+
+__attribute__((objc_root_class))
+ at interface SuperClass
++ (int)superClassMethod __attribute__((objc_direct));
+ at end
+
+ at implementation SuperClass
+
+// CHECK-LABEL: define hidden i32 @"+[SuperClass superClassMethod]"(ptr noundef %self)
++ (int)superClassMethod {
+ return 100;
+}
+
+ at end
+
+ at interface SubClass : SuperClass
++ (int)subCallerMethod __attribute__((objc_direct));
+- (int)subInstanceCaller __attribute__((objc_direct));
+ at end
+
+ at 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];
+}
+
+ at 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 <PeterRong at meta.com>
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 <PeterRong at meta.com>
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 <PeterRong at meta.com>
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 <PeterRong at meta.com>
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 <PeterRong at meta.com>
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))
- at interface ClassWithLoad
+ at interface Root
++ (int)rootDirectMethod __attribute__((objc_direct));
+ at end
+
+ at implementation Root
+
+// CHECK-LABEL: define hidden i32 @"+[Root rootDirectMethod]"(ptr noundef %self)
++ (int)rootDirectMethod { return 100; }
+
+ at end
+
+ at 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))
- at interface ClassWithoutLoad
+ at 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))
- at interface SameClassTest
+ at 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];
- at end
-
-__attribute__((objc_root_class))
- at interface SuperClass
-+ (int)superClassMethod __attribute__((objc_direct));
- at end
-
- at implementation SuperClass
-
-// CHECK-LABEL: define hidden i32 @"+[SuperClass superClassMethod]"(ptr noundef %self)
-+ (int)superClassMethod {
- return 100;
-}
-
- at end
-
- at interface SubClass : SuperClass
-+ (int)subCallerMethod __attribute__((objc_direct));
-- (int)subInstanceCaller __attribute__((objc_direct));
- at end
-
- at 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 <PeterRong at meta.com>
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 <PeterRong at meta.com>
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"
More information about the llvm-branch-commits
mailing list