[clang] [llvm] [Clang][Driver][LLVM] Add -fno-inline-functions-called-once; expose p… (PR #160343)
via llvm-commits
llvm-commits at lists.llvm.org
Wed Sep 24 02:12:52 PDT 2025
https://github.com/Karthikdhondi updated https://github.com/llvm/llvm-project/pull/160343
>From 5e58c599d47c3a15be96e76bd6bcf279e01f75b6 Mon Sep 17 00:00:00 2001
From: Karthikdhondi <karthik.dhondi at gmail.com>
Date: Wed, 24 Sep 2025 14:31:35 +0530
Subject: [PATCH 1/2] [LLVM][IPO] Add NoInlineFuncCalledOnce and plumb
-no-inline-functions-called-once via PassBuilder
Signed-off-by: Karthikdhondi <karthik.dhondi at gmail.com>
---
.../Transforms/IPO/NoInlineFuncCalledOnce.h | 18 ++++++
llvm/lib/Passes/PassBuilderPipelines.cpp | 19 ++++++
llvm/lib/Transforms/IPO/CMakeLists.txt | 1 +
.../Transforms/IPO/NoInlineFuncCalledOnce.cpp | 62 +++++++++++++++++++
4 files changed, 100 insertions(+)
create mode 100644 llvm/include/llvm/Transforms/IPO/NoInlineFuncCalledOnce.h
create mode 100644 llvm/lib/Transforms/IPO/NoInlineFuncCalledOnce.cpp
diff --git a/llvm/include/llvm/Transforms/IPO/NoInlineFuncCalledOnce.h b/llvm/include/llvm/Transforms/IPO/NoInlineFuncCalledOnce.h
new file mode 100644
index 0000000000000..41f782c02cd8c
--- /dev/null
+++ b/llvm/include/llvm/Transforms/IPO/NoInlineFuncCalledOnce.h
@@ -0,0 +1,18 @@
+#ifndef LLVM_TRANSFORMS_IPO_NOINLINEFUNCCALLEDONCE_H
+#define LLVM_TRANSFORMS_IPO_NOINLINEFUNCCALLEDONCE_H
+
+#include "llvm/IR/PassManager.h"
+#include "llvm/Support/CommandLine.h"
+
+namespace llvm {
+
+struct NoInlineFuncCalledOncePass
+ : public PassInfoMixin<NoInlineFuncCalledOncePass> {
+ PreservedAnalyses run(Module &M, ModuleAnalysisManager &);
+};
+
+// single definition of the control flag lives in the .cpp (declared here)
+extern cl::opt<bool> EnableNoInlineFuncCalledOnce;
+
+} // namespace llvm
+#endif
diff --git a/llvm/lib/Passes/PassBuilderPipelines.cpp b/llvm/lib/Passes/PassBuilderPipelines.cpp
index 30c6f06be139d..fe8dc6d810eda 100644
--- a/llvm/lib/Passes/PassBuilderPipelines.cpp
+++ b/llvm/lib/Passes/PassBuilderPipelines.cpp
@@ -66,6 +66,7 @@
#include "llvm/Transforms/IPO/MemProfContextDisambiguation.h"
#include "llvm/Transforms/IPO/MergeFunctions.h"
#include "llvm/Transforms/IPO/ModuleInliner.h"
+#include "llvm/Transforms/IPO/NoInlineFuncCalledOnce.h"
#include "llvm/Transforms/IPO/OpenMPOpt.h"
#include "llvm/Transforms/IPO/PartialInlining.h"
#include "llvm/Transforms/IPO/SCCP.h"
@@ -150,6 +151,12 @@
using namespace llvm;
+namespace llvm {
+cl::opt<bool> EnableNoInlineFuncCalledOnce(
+ "no-inline-functions-called-once", cl::init(false), cl::Hidden,
+ cl::desc("Mark TU-local functions called exactly once as noinline"));
+} // namespace llvm
+
static cl::opt<InliningAdvisorMode> UseInlineAdvisor(
"enable-ml-inliner", cl::init(InliningAdvisorMode::Default), cl::Hidden,
cl::desc("Enable ML policy for inliner. Currently trained for -Oz only"),
@@ -1274,6 +1281,9 @@ PassBuilder::buildModuleSimplificationPipeline(OptimizationLevel Level,
PGOOpt->Action == PGOOptions::SampleUse))
MPM.addPass(PGOForceFunctionAttrsPass(PGOOpt->ColdOptType));
+ if (EnableNoInlineFuncCalledOnce)
+ MPM.addPass(NoInlineFuncCalledOncePass());
+
MPM.addPass(AlwaysInlinerPass(/*InsertLifetimeIntrinsics=*/true));
if (EnableModuleInliner)
@@ -1447,6 +1457,9 @@ PassBuilder::buildModuleOptimizationPipeline(OptimizationLevel Level,
const bool LTOPreLink = isLTOPreLink(LTOPhase);
ModulePassManager MPM;
+ if (EnableNoInlineFuncCalledOnce)
+ MPM.addPass(NoInlineFuncCalledOncePass());
+
// Run partial inlining pass to partially inline functions that have
// large bodies.
if (RunPartialInlining)
@@ -1766,6 +1779,9 @@ PassBuilder::buildThinLTOPreLinkDefaultPipeline(OptimizationLevel Level) {
return MPM;
}
+ if (EnableNoInlineFuncCalledOnce)
+ MPM.addPass(NoInlineFuncCalledOncePass());
+
// Run partial inlining pass to partially inline functions that have
// large bodies.
// FIXME: It isn't clear whether this is really the right place to run this
@@ -2012,6 +2028,9 @@ PassBuilder::buildLTODefaultPipeline(OptimizationLevel Level,
// Lower variadic functions for supported targets prior to inlining.
MPM.addPass(ExpandVariadicsPass(ExpandVariadicsMode::Optimize));
+ if (EnableNoInlineFuncCalledOnce)
+ MPM.addPass(NoInlineFuncCalledOncePass());
+
// Note: historically, the PruneEH pass was run first to deduce nounwind and
// generally clean up exception handling overhead. It isn't clear this is
// valuable as the inliner doesn't currently care whether it is inlining an
diff --git a/llvm/lib/Transforms/IPO/CMakeLists.txt b/llvm/lib/Transforms/IPO/CMakeLists.txt
index 1c4ee0336d4db..50290513482ca 100644
--- a/llvm/lib/Transforms/IPO/CMakeLists.txt
+++ b/llvm/lib/Transforms/IPO/CMakeLists.txt
@@ -33,6 +33,7 @@ add_llvm_component_library(LLVMipo
MemProfContextDisambiguation.cpp
MergeFunctions.cpp
ModuleInliner.cpp
+ NoInlineFuncCalledOnce.cpp
OpenMPOpt.cpp
PartialInlining.cpp
SampleContextTracker.cpp
diff --git a/llvm/lib/Transforms/IPO/NoInlineFuncCalledOnce.cpp b/llvm/lib/Transforms/IPO/NoInlineFuncCalledOnce.cpp
new file mode 100644
index 0000000000000..44b56c3549791
--- /dev/null
+++ b/llvm/lib/Transforms/IPO/NoInlineFuncCalledOnce.cpp
@@ -0,0 +1,62 @@
+#include "llvm/Transforms/IPO/NoInlineFuncCalledOnce.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/DenseSet.h"
+#include "llvm/IR/Attributes.h"
+#include "llvm/IR/Function.h"
+#include "llvm/IR/InstIterator.h"
+#include "llvm/IR/Instructions.h"
+#include "llvm/IR/Module.h"
+#include "llvm/IR/PassManager.h"
+#include "llvm/Support/CommandLine.h"
+
+using namespace llvm;
+
+PreservedAnalyses NoInlineFuncCalledOncePass::run(Module &M,
+ ModuleAnalysisManager &) {
+ DenseMap<Function *, unsigned> DirectCalls;
+ DenseSet<Function *> Recursive;
+
+ for (Function &F : M)
+ if (!F.isDeclaration() && (F.hasInternalLinkage() || F.hasPrivateLinkage()))
+ DirectCalls[&F] = 0;
+
+ for (Function &Caller : M) {
+ if (Caller.isDeclaration())
+ continue;
+ for (Instruction &I : instructions(Caller)) {
+ auto *CB = dyn_cast<CallBase>(&I);
+ if (!CB)
+ continue;
+ const Value *Op = CB->getCalledOperand()->stripPointerCasts();
+ if (auto *Callee = const_cast<Function *>(dyn_cast<Function>(Op))) {
+ if (!DirectCalls.count(Callee))
+ continue;
+ DirectCalls[Callee] += 1;
+ if (&Caller == Callee)
+ Recursive.insert(Callee);
+ }
+ }
+ }
+
+ bool Changed = false;
+ for (auto &KV : DirectCalls) {
+ Function *F = KV.first;
+ unsigned N = KV.second;
+
+ if (N != 1)
+ continue; // only called-once
+ if (Recursive.count(F))
+ continue; // skip recursion
+ if (F->hasAddressTaken())
+ continue; // skip address-taken
+ if (F->hasFnAttribute(Attribute::AlwaysInline))
+ continue;
+ if (F->hasFnAttribute(Attribute::NoInline))
+ continue;
+
+ F->addFnAttr(Attribute::NoInline);
+ Changed = true;
+ }
+
+ return Changed ? PreservedAnalyses::none() : PreservedAnalyses::all();
+}
>From eebc35eb54b31bbfaadb4ba69b3d4ea317edfbae Mon Sep 17 00:00:00 2001
From: Karthikdhondi <karthik.dhondi at gmail.com>
Date: Wed, 24 Sep 2025 14:33:23 +0530
Subject: [PATCH 2/2] [Clang][Driver] Add -fno-inline-functions-called-once;
expose positive form in --help
Signed-off-by: Karthikdhondi <karthik.dhondi at gmail.com>
---
clang/include/clang/Driver/Options.td | 8 +++--
clang/lib/Driver/ToolChains/Clang.cpp | 6 ++++
.../test/CodeGen/no-inline-func-called-once.c | 32 +++++++++++++++++++
clang/test/Driver/clang_f_opts.c | 3 --
.../Driver/fno-inline-functions-called-once.c | 20 ++++++++++++
5 files changed, 64 insertions(+), 5 deletions(-)
create mode 100644 clang/test/CodeGen/no-inline-func-called-once.c
create mode 100644 clang/test/Driver/fno-inline-functions-called-once.c
diff --git a/clang/include/clang/Driver/Options.td b/clang/include/clang/Driver/Options.td
index 16e1c396fedbe..6c6d5f8455d4f 100644
--- a/clang/include/clang/Driver/Options.td
+++ b/clang/include/clang/Driver/Options.td
@@ -6959,8 +6959,12 @@ defm merge_constants : BooleanFFlag<"merge-constants">, Group<clang_ignored_gcc_
defm modulo_sched : BooleanFFlag<"modulo-sched">, Group<clang_ignored_gcc_optimization_f_Group>;
defm modulo_sched_allow_regmoves : BooleanFFlag<"modulo-sched-allow-regmoves">,
Group<clang_ignored_gcc_optimization_f_Group>;
-defm inline_functions_called_once : BooleanFFlag<"inline-functions-called-once">,
- Group<clang_ignored_gcc_optimization_f_Group>;
+defm inline_functions_called_once
+ : BooleanFFlag<"inline-functions-called-once">,
+ Group<f_clang_Group>,
+ Visibility<[ClangOption, CC1Option]>,
+ HelpText<"Control inlining of TU-local functions called exactly once "
+ "(use -fno-inline-functions-called-once to inhibit it)">;
def finline_limit_EQ : Joined<["-"], "finline-limit=">, Group<clang_ignored_gcc_optimization_f_Group>;
defm finline_limit : BooleanFFlag<"inline-limit">, Group<clang_ignored_gcc_optimization_f_Group>;
defm inline_small_functions : BooleanFFlag<"inline-small-functions">,
diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp
index f67454ee517bd..78f2f209b7165 100644
--- a/clang/lib/Driver/ToolChains/Clang.cpp
+++ b/clang/lib/Driver/ToolChains/Clang.cpp
@@ -7266,6 +7266,12 @@ void Clang::ConstructJob(Compilation &C, const JobAction &JA,
Args.AddLastArg(CmdArgs, options::OPT_finline_max_stacksize_EQ);
+ // Forward -fno-inline-functions-called-once to LLVM so the pass is enabled.
+ if (Args.hasArg(options::OPT_fno_inline_functions_called_once)) {
+ CmdArgs.push_back("-mllvm");
+ CmdArgs.push_back("-no-inline-functions-called-once");
+ }
+
// FIXME: Find a better way to determine whether we are in C++20.
bool HaveCxx20 =
Std &&
diff --git a/clang/test/CodeGen/no-inline-func-called-once.c b/clang/test/CodeGen/no-inline-func-called-once.c
new file mode 100644
index 0000000000000..92cdb57297739
--- /dev/null
+++ b/clang/test/CodeGen/no-inline-func-called-once.c
@@ -0,0 +1,32 @@
+// REQUIRES: x86-registered-target
+// RUN: %clang -O1 -S -emit-llvm %s -fno-inline-functions-called-once -o - | FileCheck %s --check-prefix=NOINLINE
+
+// We verify three things:
+// 1) There is a surviving call to bad_function (so it wasn’t inlined).
+// 2) bad_function’s definition exists and carries an attribute group id.
+// 3) That attribute group includes 'noinline'.
+
+// The call is earlier in the IR than the callee/attributes, so use -DAG for the
+// first two checks to avoid order constraints, then pin the attributes match.
+
+// NOINLINE-DAG: call{{.*}} @bad_function{{.*}}
+// NOINLINE-DAG: define internal{{.*}} @bad_function{{.*}} #[[ATTR:[0-9]+]]
+// NOINLINE: attributes #[[ATTR]] = { {{.*}}noinline{{.*}} }
+
+volatile int G;
+
+static void bad_function(void) {
+ // Volatile side effect ensures the call can’t be DCE’d.
+ G++;
+}
+
+static void test(void) {
+ // Exactly one TU-local caller of bad_function.
+ bad_function();
+}
+
+int main(void) {
+ // Make the caller reachable so it survives global DCE.
+ test();
+ return 0;
+}
diff --git a/clang/test/Driver/clang_f_opts.c b/clang/test/Driver/clang_f_opts.c
index bdeb747aa66a3..39726ac78140c 100644
--- a/clang/test/Driver/clang_f_opts.c
+++ b/clang/test/Driver/clang_f_opts.c
@@ -277,7 +277,6 @@
// RUN: -fgcse-las \
// RUN: -fgcse-sm \
// RUN: -fipa-cp \
-// RUN: -finline-functions-called-once \
// RUN: -fmodulo-sched \
// RUN: -fmodulo-sched-allow-regmoves \
// RUN: -fpeel-loops \
@@ -349,7 +348,6 @@
// RUN: -fgcse-las \
// RUN: -fgcse-sm \
// RUN: -fipa-cp \
-// RUN: -finline-functions-called-once \
// RUN: -fmodulo-sched \
// RUN: -fmodulo-sched-allow-regmoves \
// RUN: -fpeel-loops \
@@ -409,7 +407,6 @@
// CHECK-WARNING-DAG: optimization flag '-fgcse-las' is not supported
// CHECK-WARNING-DAG: optimization flag '-fgcse-sm' is not supported
// CHECK-WARNING-DAG: optimization flag '-fipa-cp' is not supported
-// CHECK-WARNING-DAG: optimization flag '-finline-functions-called-once' is not supported
// CHECK-WARNING-DAG: optimization flag '-fmodulo-sched' is not supported
// CHECK-WARNING-DAG: optimization flag '-fmodulo-sched-allow-regmoves' is not supported
// CHECK-WARNING-DAG: optimization flag '-fpeel-loops' is not supported
diff --git a/clang/test/Driver/fno-inline-functions-called-once.c b/clang/test/Driver/fno-inline-functions-called-once.c
new file mode 100644
index 0000000000000..d866f9f83358b
--- /dev/null
+++ b/clang/test/Driver/fno-inline-functions-called-once.c
@@ -0,0 +1,20 @@
+// REQUIRES: x86-registered-target
+
+// Check that -fno-inline-functions-called-once is forwarded to LLVM.
+// RUN: %clang -### -S %s -fno-inline-functions-called-once 2>&1 \
+// RUN: | FileCheck %s --check-prefix=FWD
+// FWD: "-mllvm" "-no-inline-functions-called-once"
+
+// Check that the positive form does NOT forward anything to -mllvm.
+// RUN: %clang -### -S %s -finline-functions-called-once 2>&1 \
+// RUN: | FileCheck %s --check-prefix=POS
+// POS-NOT: -mllvm
+// POS-NOT: -no-inline-functions-called-once
+
+// Help text should show both flags (order-independent).
+// RUN: %clang --help 2>&1 | FileCheck %s --check-prefix=HELP
+// HELP-DAG: -finline-functions-called-once
+// HELP-DAG: -fno-inline-functions-called-once
+
+int x;
+
More information about the llvm-commits
mailing list