[llvm-branch-commits] [clang] [ExposeObjCDirect] Optimizations (PR #170619)
Peter Rong via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Thu Dec 4 13:13:07 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/4] [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/4] 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/4] 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/4] 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
More information about the llvm-branch-commits
mailing list