[llvm-branch-commits] [clang] [ExposeObjCDirect] Optimizations (PR #170619)
via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Tue Dec 9 09:37:22 PST 2025
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clang-codegen
Author: Peter Rong (DataCorrupted)
<details>
<summary>Changes</summary>
## TL;DR
This is a stack of PRs implementing features to expose direct methods ABI.
You can see the RFC, design, and discussion [here](https://discourse.llvm.org/t/rfc-optimizing-code-size-of-objc-direct-by-exposing-function-symbols-and-moving-nil-checks-to-thunks/88866).
https://github.com/llvm/llvm-project/pull/170616 Flag `-fexpose-objc-direct` set up
https://github.com/llvm/llvm-project/pull/170617 Code refactoring to ease later reviews
https://github.com/llvm/llvm-project/pull/170618 Thunk generation
https://github.com/llvm/llvm-project/pull/170619 **Optimizations, some class objects can be known to be realized**
## Implementation details
Two heuristics where we can infer the class is definitely realized.
For all non-weak-link classe:
1. If it has `+load` defined
2. If the callee is in the same class as the caller, or in the super class of the caller.
## Tests
- `expose-direct-method-opt-class-realization.m`
---
Full diff: https://github.com/llvm/llvm-project/pull/170619.diff
6 Files Affected:
- (modified) clang/lib/CodeGen/CGObjCMac.cpp (+14)
- (modified) clang/lib/CodeGen/CGObjCRuntime.cpp (+82-4)
- (modified) clang/lib/CodeGen/CGObjCRuntime.h (+28-5)
- (modified) clang/lib/CodeGen/CodeGenFunction.h (+9)
- (added) clang/test/CodeGenObjC/expose-direct-method-opt-class-realization.m (+167)
- (modified) clang/test/CodeGenObjC/expose-direct-method.m (+4-3)
``````````diff
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 a4b4460fdc49c..82780e3268f9e 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 (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;
@@ -415,11 +417,87 @@ bool CGObjCRuntime::canMessageReceiverBeNull(
bool CGObjCRuntime::canClassObjectBeUnrealized(
const ObjCInterfaceDecl *CalleeClassDecl, CodeGenFunction &CGF) const {
- // TODO
+ if (!CalleeClassDecl || isWeakLinkedClass(CalleeClassDecl))
+ return true;
+
+ // 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
+ // 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 classes
+ // If we've already emitted a class method call for this class (or a subclass)
+ // earlier, then the class must be realized.
+ //
+ // 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;
}
+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 d3d4745cb77a7..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).
@@ -226,7 +236,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 +340,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.
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
new file mode 100644
index 0000000000000..cf43123b3ab40
--- /dev/null
+++ b/clang/test/CodeGenObjC/expose-direct-method-opt-class-realization.m
@@ -0,0 +1,167 @@
+// 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 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));
+ 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
+ at interface ClassWithoutLoad : Root
++ (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.
+// ============================================================================
+
+ at interface SameClassTest : Root
++ (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"
+ 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)
+- (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"
+ int a = [SameClassTest classDirectMethod];
+
+ // 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 @"+[Root rootDirectMethod]"(ptr noundef
+ // CHECK-NOT: call i32 @"+[Root rootDirectMethod]_thunk"
+ int b = [Root rootDirectMethod];
+
+ return a + b;
+}
+
+ at 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 3d1420619774b..fceedf4e944c0 100644
--- a/clang/test/CodeGenObjC/expose-direct-method.m
+++ b/clang/test/CodeGenObjC/expose-direct-method.m
@@ -266,10 +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: 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
);
}
@@ -289,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"
``````````
</details>
https://github.com/llvm/llvm-project/pull/170619
More information about the llvm-branch-commits
mailing list