https://github.com/ahatanak updated https://github.com/llvm/llvm-project/pull/183922
>From e1ff648d758ca8b97364e3f39911a04bd20b8a92 Mon Sep 17 00:00:00 2001 From: Ahmed Bougacha <[email protected]> Date: Sat, 28 Feb 2026 06:18:39 -0800 Subject: [PATCH] [ObjC] Support emission of selector stubs calls instead of objc_msgSend. This optimizes objc_msgSend calls by emitting "selector stubs" instead. Usually, the linker redirects calls to external symbols to a symbol stub it generates, which loads the target function's address from the GOT and branches to it: <symbol stub for _func:> adrp x16, _func@GOTPAGE ldr x16, [x16, _func@GOTPAGEOFF] br x16 with msgSend selector stubs, we extend that to compute the selector as well: <selector stub for "foo":> adrp x1, <selector ref for "foo">@PAGE ldr x1, [x1, <selector ref for "foo">@PAGEOFF] adrp x16, _objc_msgSend@GOTPAGE ldr x16, [x16, _objc_msgSend@GOTPAGEOFF] br x16 This lets us avoid loading the selector in the compiler, hopefully leading to codesize reductions. We're considering two kinds of stubs, the combined one above with the objc_msgSend dispatch included, or a shorter one that forwards to the existing stub for objc_msgSend. That's a decision to be made in the linker. Concretely, instead of something like: %sel = load i8*, i8** @<selector ref for "foo"> call ... @objc_msgSend(i8* self, i8* sel) we emit a call to a newly-declared external "function": call ... @"objc_msgSend$foo"(i8* self, i8* undef) where "objc_msgSend$foo" is treated as any other external symbol reference throughout the compiler, and, at link-time, is recognized by the linker as a selector stub. There are other ways to implement this. An obvious one is to combine away the objc_msgSend(self, load(sel)) pattern at the IR level. Both seem feasible, but the IRGen approach might save us a tiny bit of compile-time, with the assumption that loads need more mid-level analysis than boring external functions. This optimization requires linker version 811.2 or newer for arm64, arm64e, and arm64_32. rdar://84437635 --- clang/include/clang/Basic/CodeGenOptions.def | 1 + clang/include/clang/Options/Options.td | 3 + clang/lib/CodeGen/CGObjCMac.cpp | 36 ++++- clang/lib/Driver/ToolChains/Clang.cpp | 8 ++ clang/lib/Driver/ToolChains/Darwin.cpp | 8 ++ clang/test/CodeGenObjC/method-selector-stub.m | 131 ++++++++++++++++++ .../test/Driver/darwin-objc-selector-stubs.m | 42 ++++++ 7 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 clang/test/CodeGenObjC/method-selector-stub.m create mode 100644 clang/test/Driver/darwin-objc-selector-stubs.m diff --git a/clang/include/clang/Basic/CodeGenOptions.def b/clang/include/clang/Basic/CodeGenOptions.def index 5e174b21be466..b1e6bdbc99cae 100644 --- a/clang/include/clang/Basic/CodeGenOptions.def +++ b/clang/include/clang/Basic/CodeGenOptions.def @@ -214,6 +214,7 @@ CODEGENOPT(NoZeroInitializedInBSS , 1, 0, Benign) ///< -fno-zero-initialized-in- ENUM_CODEGENOPT(ObjCDispatchMethod, ObjCDispatchMethodKind, 2, Legacy, Benign) /// Replace certain message sends with calls to ObjC runtime entrypoints CODEGENOPT(ObjCConvertMessagesToRuntimeCalls , 1, 1, Benign) +CODEGENOPT(ObjCMsgSendSelectorStubs , 1, 0, Benign) ///< Use per-selector linker stubs for objc_msgSend CODEGENOPT(ObjCAvoidHeapifyLocalBlocks, 1, 0, Benign) /// Generate direct method precondition thunks to expose symbols and optimize nil checks. CODEGENOPT(ObjCDirectPreconditionThunk, 1, 0, Benign) diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td index c8a1e478122e1..ebec192f36788 100644 --- a/clang/include/clang/Options/Options.td +++ b/clang/include/clang/Options/Options.td @@ -3844,6 +3844,9 @@ defm objc_convert_messages_to_runtime_calls : BoolFOption<"objc-convert-messages CodeGenOpts<"ObjCConvertMessagesToRuntimeCalls">, DefaultTrue, NegFlag<SetFalse, [], [ClangOption, CC1Option]>, PosFlag<SetTrue>>; +defm objc_msgsend_selector_stubs : BoolFOption<"objc-msgsend-selector-stubs", + CodeGenOpts<"ObjCMsgSendSelectorStubs">, DefaultFalse, + PosFlag<SetTrue, [], [ClangOption, CC1Option]>, NegFlag<SetFalse>>; defm objc_arc_exceptions : BoolFOption<"objc-arc-exceptions", CodeGenOpts<"ObjCAutoRefCountExceptions">, DefaultFalse, PosFlag<SetTrue, [], [ClangOption, CC1Option], diff --git a/clang/lib/CodeGen/CGObjCMac.cpp b/clang/lib/CodeGen/CGObjCMac.cpp index e6c244547cefd..0ffecec236a2b 100644 --- a/clang/lib/CodeGen/CGObjCMac.cpp +++ b/clang/lib/CodeGen/CGObjCMac.cpp @@ -863,6 +863,9 @@ class CGObjCCommonMac : public CodeGen::CGObjCRuntime { llvm::DenseMap<const ObjCMethodDecl *, DirectMethodInfo> DirectMethodDefinitions; + /// MethodSelectorStubs - map of selector stub functions + llvm::DenseMap<Selector, llvm::Function *> MethodSelectorStubs; + /// PropertyNames - uniqued method variable names. llvm::DenseMap<IdentifierInfo *, llvm::GlobalVariable *> PropertyNames; @@ -1082,6 +1085,10 @@ class CGObjCCommonMac : public CodeGen::CGObjCRuntime { const ObjCMethodDecl *OMD, const ObjCContainerDecl *CD) override; + llvm::Function * + GenerateMethodSelectorStub(Selector Sel, + const ObjCCommonTypesHelper &ObjCTypes); + void GenerateProtocol(const ObjCProtocolDecl *PD) override; /// GetOrEmitProtocolRef - Get a forward reference to the protocol @@ -2123,8 +2130,15 @@ CodeGen::RValue CGObjCCommonMac::EmitMessageSend( // must be made for it. if (ReceiverCanBeNull && CGM.ReturnTypeUsesSRet(MSI.CallInfo)) RequiresNullCheck = true; - Fn = (ObjCABI == 2) ? ObjCTypes.getSendFn2(IsSuper) - : ObjCTypes.getSendFn(IsSuper); + if (!IsSuper && CGM.getCodeGenOpts().ObjCMsgSendSelectorStubs) { + // Try to use a selector stub declaration instead of objc_msgSend. + Fn = GenerateMethodSelectorStub(Sel, ObjCTypes); + // Selector stubs synthesize `_cmd` in the stub, so we don't have to. + RequiresSelValue = false; + } else { + Fn = (ObjCABI == 2) ? ObjCTypes.getSendFn2(IsSuper) + : ObjCTypes.getSendFn(IsSuper); + } } // Cast function to proper signature @@ -4047,6 +4061,24 @@ void CGObjCCommonMac::GenerateDirectMethodPrologue( } } +llvm::Function *CGObjCCommonMac::GenerateMethodSelectorStub( + Selector Sel, const ObjCCommonTypesHelper &ObjCTypes) { + auto I = MethodSelectorStubs.find(Sel); + + if (I != MethodSelectorStubs.end()) + return I->second; + + auto *FnTy = llvm::FunctionType::get( + ObjCTypes.ObjectPtrTy, {ObjCTypes.ObjectPtrTy, ObjCTypes.SelectorPtrTy}, + /*IsVarArg=*/true); + auto *Fn = cast<llvm::Function>( + CGM.CreateRuntimeFunction(FnTy, "objc_msgSend$" + Sel.getAsString()) + .getCallee()); + + MethodSelectorStubs.insert(std::make_pair(Sel, Fn)); + return Fn; +} + llvm::GlobalVariable * CGObjCCommonMac::CreateMetadataVar(Twine Name, ConstantStructBuilder &Init, StringRef Section, CharUnits Align, diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp index 0aa93e2e46814..7e043d78072d0 100644 --- a/clang/lib/Driver/ToolChains/Clang.cpp +++ b/clang/lib/Driver/ToolChains/Clang.cpp @@ -4155,6 +4155,14 @@ static void RenderObjCOptions(const ToolChain &TC, const Driver &D, << "-fobjc-direct-precondition-thunk" << Runtime.getAsString(); } } + + // Pass down -fobjc-msgsend-selector-stubs if present. + if (types::isObjC(Input.getType())) { + if (Args.hasFlag(options::OPT_fobjc_msgsend_selector_stubs, + options::OPT_fno_objc_msgsend_selector_stubs, false)) + CmdArgs.push_back("-fobjc-msgsend-selector-stubs"); + } + // When ObjectiveC legacy runtime is in effect on MacOSX, turn on the option // to do Array/Dictionary subscripting by default. if (Arch == llvm::Triple::x86 && T.isMacOSX() && diff --git a/clang/lib/Driver/ToolChains/Darwin.cpp b/clang/lib/Driver/ToolChains/Darwin.cpp index aec1ad7d2f155..810637ef3bf05 100644 --- a/clang/lib/Driver/ToolChains/Darwin.cpp +++ b/clang/lib/Driver/ToolChains/Darwin.cpp @@ -3383,6 +3383,14 @@ void Darwin::addClangTargetOptions( isAlignedAllocationUnavailable()) CC1Args.push_back("-faligned-alloc-unavailable"); + // Enable objc_msgSend selector stubs by default if the linker supports it. + // ld64-811.2+ does, for arm64, arm64e, and arm64_32. + if (!DriverArgs.hasArgNoClaim(options::OPT_fobjc_msgsend_selector_stubs, + options::OPT_fno_objc_msgsend_selector_stubs) && + getTriple().isAArch64() && + (getLinkerVersion(DriverArgs) >= VersionTuple(811, 2))) + CC1Args.push_back("-fobjc-msgsend-selector-stubs"); + // Pass "-fno-sized-deallocation" only when the user hasn't manually enabled // or disabled sized deallocations. if (!DriverArgs.hasArgNoClaim(options::OPT_fsized_deallocation, diff --git a/clang/test/CodeGenObjC/method-selector-stub.m b/clang/test/CodeGenObjC/method-selector-stub.m new file mode 100644 index 0000000000000..9359b70832ad3 --- /dev/null +++ b/clang/test/CodeGenObjC/method-selector-stub.m @@ -0,0 +1,131 @@ +// RUN: %clang_cc1 -emit-llvm -fobjc-msgsend-selector-stubs -triple arm64-apple-ios15 %s -o - | FileCheck %s +// RUN: %clang_cc1 -emit-llvm -fobjc-msgsend-selector-stubs -triple arm64_32-apple-watchos8 %s -o - | FileCheck %s + +__attribute__((objc_root_class)) +@interface Root +- (int)test0; ++ (int)class0; +- (int)test1: (int)a0; +- (int)test2: (int)a0 withA: (int)a1; + +@property(readonly) int intProperty; +@end + +@interface Foo : Root +@end + +@interface Foo () +- (int)testSuper0; +- (int)methodInExtension; +@end + +@interface Foo (Cat) +- (int)methodInCategory; +@end + + +// CHECK: [[TEST0_METHNAME:@OBJC_METH_VAR_NAME_[^ ]*]] = private unnamed_addr constant [6 x i8] c"test0\00", section "__TEXT,__objc_methname,cstring_literals" +// CHECK: [[TEST0_SELREF:@OBJC_SELECTOR_REFERENCES_[^ ]*]] = internal externally_initialized global ptr [[TEST0_METHNAME]], section "__DATA,__objc_selrefs,literal_pointers,no_dead_strip" + +@implementation Foo + +- (int)testSuper0 { + // Super calls don't have stubs. + // CHECK-LABEL: define{{.*}} i32 @"\01-[Foo testSuper0]"( + // CHECK: [[SEL:%[^ ]]] = load ptr, ptr [[TEST0_SELREF]] + // CHECK: %{{[^ ]*}} = call i32 @objc_msgSendSuper2(ptr {{[^,]+}}, ptr {{[^,)]*}}[[SEL]]) + + return [super test0]; +} + +// CHECK-LABEL: define internal i32 @"\01-[Foo methodInExtension]"( +- (int)methodInExtension { + return 42; +} +@end + +@implementation Foo (Cat) +// CHECK-LABEL: define internal i32 @"\01-[Foo(Cat) methodInCategory]"( +- (int)methodInCategory { + return 42; +} +// CHECK-LABEL: define internal i32 @"\01-[Foo(Cat) methodInCategoryNoDecl]"( +- (int)methodInCategoryNoDecl { + return 42; +} +@end + +int test_root_test0(Root *r) { + // CHECK-LABEL: define{{.*}} i32 @test_root_test0( + // CHECK: %{{[^ ]*}} = call i32 @"objc_msgSend$test0"(ptr {{[^,]+}}, ptr {{[^,)]*}}undef) + return [r test0]; +} + +// CHECK: declare ptr @"objc_msgSend$test0"(ptr, ptr, ...) + +int test_root_class0() { + // CHECK-LABEL: define{{.*}} i32 @test_root_class0( + // CHECK: %{{[^ ]*}} = call i32 @"objc_msgSend$class0"(ptr {{[^,]+}}, ptr {{[^,)]*}}undef) + return [Root class0]; +} + +// CHECK: declare ptr @"objc_msgSend$class0"(ptr, ptr, ...) + +int test_root_test1(Root *r) { + // CHECK-LABEL: define{{.*}} i32 @test_root_test1( + // CHECK: %{{[^ ]*}} = call i32 @"objc_msgSend$test1:"(ptr {{[^,]+}}, ptr {{[^,)]*}}undef, i32 {{[^,)]*}}42) + return [r test1: 42]; +} + +// CHECK: declare ptr @"objc_msgSend$test1:"(ptr, ptr, ...) + +int test_root_test2(Root *r) { + // CHECK-LABEL: define{{.*}} i32 @test_root_test2( + // CHECK: %{{[^ ]*}} = call i32 @"objc_msgSend$test2:withA:"(ptr {{[^,]+}}, ptr {{[^,)]*}}undef, i32 {{[^,)]*}}42, i32 {{[^,)]*}}84) + return [r test2: 42 withA: 84]; + +} + +// CHECK: declare ptr @"objc_msgSend$test2:withA:"(ptr, ptr, ...) + +int test_extension(Foo *f) { + // CHECK-LABEL: define{{.*}} i32 @test_extension + // CHECK: %{{[^ ]*}} = call i32 @"objc_msgSend$methodInExtension"(ptr {{[^,]+}}, ptr {{[^,)]*}}undef) + return [f methodInExtension]; +} + +// CHECK: declare ptr @"objc_msgSend$methodInExtension"(ptr, ptr, ...) + +int test_category(Foo *f) { + // CHECK-LABEL: define{{.*}} i32 @test_category + // CHECK: %{{[^ ]*}} = call i32 @"objc_msgSend$methodInCategory"(ptr {{[^,]+}}, ptr {{[^,)]*}}undef) + return [f methodInCategory]; +} + +// CHECK: declare ptr @"objc_msgSend$methodInCategory"(ptr, ptr, ...) + +int test_category_nodecl(Foo *f) { + // CHECK-LABEL: define{{.*}} i32 @test_category_nodecl + // CHECK: %{{[^ ]*}} = call i32 @"objc_msgSend$methodInCategoryNoDecl"(ptr {{[^,]+}}, ptr {{[^,)]*}}undef) + return [f methodInCategoryNoDecl]; +} + +// CHECK: declare ptr @"objc_msgSend$methodInCategoryNoDecl"(ptr, ptr, ...) + + +// === Test the special case where there's no method, but only a selector. + +@interface NSArray +@end; + +extern void use(id); + +void test_fastenum_rawsel(NSArray *array) { + // CHECK-LABEL: define{{.*}} void @test_fastenum_rawsel + // CHECK: %{{[^ ]*}} = call {{i32|i64}} @"objc_msgSend$countByEnumeratingWithState:objects:count:"(ptr {{[^,]+}}, ptr + // CHECK-NOT: @objc_msgSend to + for (id x in array) + use(x); +} + +// CHECK: declare ptr @"objc_msgSend$countByEnumeratingWithState:objects:count:"(ptr, ptr, ...) diff --git a/clang/test/Driver/darwin-objc-selector-stubs.m b/clang/test/Driver/darwin-objc-selector-stubs.m new file mode 100644 index 0000000000000..47d9473bffed3 --- /dev/null +++ b/clang/test/Driver/darwin-objc-selector-stubs.m @@ -0,0 +1,42 @@ +// Check default enablement of Objective-C objc_msgSend selector stubs codegen. + +// Enabled by default with ld64-811.2+ ... + +// ... for arm64 +// RUN: %clang -target arm64-apple-ios15 -mlinker-version=811.2 -### %s 2>&1 | FileCheck %s +// RUN: %clang -target arm64-apple-ios15 -mlinker-version=811 -### %s 2>&1 | FileCheck %s --check-prefix=NOSTUBS + +// RUN: %clang -target arm64-apple-macos12 -mlinker-version=811.2 -### %s 2>&1 | FileCheck %s +// RUN: %clang -target arm64-apple-macos12 -mlinker-version=811 -### %s 2>&1 | FileCheck %s --check-prefix=NOSTUBS + +// ... for arm64e +// RUN: %clang -target arm64e-apple-ios15 -mlinker-version=811.2 -### %s 2>&1 | FileCheck %s +// RUN: %clang -target arm64e-apple-ios15 -mlinker-version=811 -### %s 2>&1 | FileCheck %s --check-prefix=NOSTUBS + +// ... and arm64_32. +// RUN: %clang -target arm64_32-apple-watchos8 -mlinker-version=811.2 -### %s 2>&1 | FileCheck %s +// RUN: %clang -target arm64_32-apple-watchos8 -mlinker-version=811 -### %s 2>&1 | FileCheck %s --check-prefix=NOSTUBS + + +// Disabled elsewhere, e.g. x86_64. +// RUN: %clang -target x86_64-apple-macos12 -mlinker-version=811.2 -### %s 2>&1 | FileCheck %s --check-prefix=NOSTUBS +// RUN: %clang -target x86_64-apple-macos12 -mlinker-version=811 -### %s 2>&1 | FileCheck %s --check-prefix=NOSTUBS + +// RUN: %clang -target x86_64-apple-ios15-simulator -mlinker-version=811.2 -### %s 2>&1 | FileCheck %s --check-prefix=NOSTUBS +// RUN: %clang -target x86_64-apple-ios15-simulator -mlinker-version=811 -### %s 2>&1 | FileCheck %s --check-prefix=NOSTUBS + +// ... or armv7k. +// RUN: %clang -target armv7k-apple-watchos6 -mlinker-version=811.2 -### %s 2>&1 | FileCheck %s --check-prefix=NOSTUBS +// RUN: %clang -target armv7k-apple-watchos6 -mlinker-version=811 -### %s 2>&1 | FileCheck %s --check-prefix=NOSTUBS + + +// Enabled if you ask for it. +// RUN: %clang -target arm64-apple-macos12 -fobjc-msgsend-selector-stubs -### %s 2>&1 | FileCheck %s +// RUN: %clang -target arm64-apple-macos12 -fobjc-msgsend-selector-stubs -mlinker-version=0 -### %s 2>&1 | FileCheck %s + +// Disabled if you ask for that. +// RUN: %clang -target arm64-apple-macos12 -fno-objc-msgsend-selector-stubs -mlinker-version=811.2 -### %s 2>&1 | FileCheck %s --check-prefix=NOSTUBS + + +// CHECK: "-fobjc-msgsend-selector-stubs" +// NOSTUBS-NOT: objc-msgsend-selector-stubs _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
