[llvm] [opt] Add --save-stats option (PR #167304)

Tomer Shafir via llvm-commits llvm-commits at lists.llvm.org
Mon Nov 10 03:59:20 PST 2025


https://github.com/tomershafir created https://github.com/llvm/llvm-project/pull/167304

This patch adds a Clang-compatible --save-stats option to opt, to provide an easy to use way to save LLVM statistics files when working with opt on the middle end.

This is a follow up on the addition to `llc`: https://github.com/llvm/llvm-project/pull/163967

Like on Clang, one can specify --save-stats, --save-stats=cwd, and --save-stats=obj with the same semantics and JSON format. The pre-existing --stats option is not affected.

The implementation extracts the flag and its methods into the common `CodeGen/CommandFlags` as `LLVM_ABI`, using a new registration class to conservatively enable opt-in rather than let all tools take it. Its only needed for llc and opt for now. Then it refactors llc and adds support for opt.

>From c97d8a2a9f85f897bc880515b033fbe87ef2dc76 Mon Sep 17 00:00:00 2001
From: tomershafir <tomer.shafir8 at gmail.com>
Date: Mon, 10 Nov 2025 13:51:40 +0200
Subject: [PATCH] [opt] Add --save-stats option

This patch adds a Clang-compatible --save-stats option to opt, to provide an easy to use way to save LLVM statistics files when working with opt on the middle end.

This is a follow up on the addition to `llc`: https://github.com/llvm/llvm-project/pull/163967

Like on Clang, one can specify --save-stats, --save-stats=cwd, and --save-stats=obj with the same semantics and JSON format. The pre-existing --stats option is not affected.

The implementation extracts the flag and its methods into the common `CodeGen/CommandFlags` as `LLVM_ABI`, using a new registration class to conservatively enable opt-in rather than let all tools take it. Its only needed for llc and opt for now. Then it refactors llc and adds support for opt.
---
 llvm/docs/CommandGuide/opt.rst           |  6 ++
 llvm/docs/ReleaseNotes.md                |  1 +
 llvm/include/llvm/CodeGen/CommandFlags.h | 20 +++++++
 llvm/lib/CodeGen/CommandFlags.cpp        | 74 ++++++++++++++++++++++--
 llvm/test/tools/opt/save-stats.ll        | 15 +++++
 llvm/tools/llc/llc.cpp                   | 59 +------------------
 llvm/tools/opt/optdriver.cpp             | 14 +++--
 7 files changed, 123 insertions(+), 66 deletions(-)
 create mode 100644 llvm/test/tools/opt/save-stats.ll

diff --git a/llvm/docs/CommandGuide/opt.rst b/llvm/docs/CommandGuide/opt.rst
index da93b8e4e9c54..eb15f0ea43634 100644
--- a/llvm/docs/CommandGuide/opt.rst
+++ b/llvm/docs/CommandGuide/opt.rst
@@ -70,6 +70,12 @@ OPTIONS
 
  Print statistics.
 
+.. option:: --save-stats, --save-stats=cwd, --save-stats=obj
+
+ Save LLVM statistics to a file in the current directory
+ (:option:`--save-stats`/"--save-stats=cwd") or the directory
+ of the output file ("--save-stats=obj") in JSON format.
+
 .. option:: -time-passes
 
  Record the amount of time needed for each pass and print it to standard
diff --git a/llvm/docs/ReleaseNotes.md b/llvm/docs/ReleaseNotes.md
index fd78c97c86d24..1ae539fec0fef 100644
--- a/llvm/docs/ReleaseNotes.md
+++ b/llvm/docs/ReleaseNotes.md
@@ -183,6 +183,7 @@ Changes to the LLVM tools
 * Some code paths for supporting Python 2.7 in `llvm-lit` have been removed.
 * Support for `%T` in lit has been removed.
 * Add `--save-stats` option to `llc` to save LLVM statistics to a file. Compatible with the Clang option.
+* Add `--save-stats` option to `opt` to save LLVM statistics to a file. Compatible with the Clang option.
 
 * `llvm-config` gained a new flag `--quote-paths` which quotes and escapes paths
   emitted on stdout, to account for spaces or other special characters in path.
diff --git a/llvm/include/llvm/CodeGen/CommandFlags.h b/llvm/include/llvm/CodeGen/CommandFlags.h
index af66f2d5776ba..e46628bdf1d6e 100644
--- a/llvm/include/llvm/CodeGen/CommandFlags.h
+++ b/llvm/include/llvm/CodeGen/CommandFlags.h
@@ -154,12 +154,22 @@ LLVM_ABI bool getJMCInstrument();
 
 LLVM_ABI bool getXCOFFReadOnlyPointers();
 
+enum SaveStatsMode { None, Cwd, Obj };
+
+LLVM_ABI SaveStatsMode getSaveStats();
+
 /// Create this object with static storage to register codegen-related command
 /// line options.
 struct RegisterCodeGenFlags {
   LLVM_ABI RegisterCodeGenFlags();
 };
 
+/// Tools that support stats saving should create this object with static storage 
+/// to register the --save-stats command line option. 
+struct RegisterSaveStatsFlag {
+  LLVM_ABI RegisterSaveStatsFlag();
+};
+
 LLVM_ABI bool getEnableBBAddrMap();
 
 LLVM_ABI llvm::BasicBlockSection
@@ -203,6 +213,16 @@ LLVM_ABI Expected<std::unique_ptr<TargetMachine>> createTargetMachineForTriple(
     StringRef TargetTriple,
     CodeGenOptLevel OptLevel = CodeGenOptLevel::Default);
 
+/// Conditionally enables the collection of LLVM statistics during the tool run,
+/// based on the value of the flag. Must be call before the tool run to actually
+/// collect data.
+LLVM_ABI int MaybeEnableStatistics();
+
+/// Conditionally saves the collected LLVM statistics to the recevied output file,
+/// based on the value of the flag. Shuould be called after the tool run, and must
+/// follow a call to `MaybeEnableStats()` to actually have data to write.
+LLVM_ABI int MaybeSaveStatistics(StringRef OutputFilename, StringRef ToolName);
+
 } // namespace codegen
 } // namespace llvm
 
diff --git a/llvm/lib/CodeGen/CommandFlags.cpp b/llvm/lib/CodeGen/CommandFlags.cpp
index c1365f499dcf5..caa2f3cf8cd89 100644
--- a/llvm/lib/CodeGen/CommandFlags.cpp
+++ b/llvm/lib/CodeGen/CommandFlags.cpp
@@ -13,33 +13,43 @@
 //===----------------------------------------------------------------------===//
 
 #include "llvm/CodeGen/CommandFlags.h"
+#include "llvm/ADT/SmallString.h"
+#include "llvm/ADT/Statistic.h"
 #include "llvm/ADT/StringExtras.h"
+#include "llvm/ADT/StringRef.h"
 #include "llvm/IR/Instructions.h"
 #include "llvm/IR/Intrinsics.h"
 #include "llvm/IR/Module.h"
 #include "llvm/MC/MCTargetOptionsCommandFlags.h"
 #include "llvm/MC/TargetRegistry.h"
 #include "llvm/Support/CommandLine.h"
+#include "llvm/Support/FileSystem.h"
 #include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/raw_ostream.h"
+#include "llvm/Support/WithColor.h"
 #include "llvm/Target/TargetMachine.h"
 #include "llvm/TargetParser/Host.h"
 #include "llvm/TargetParser/SubtargetFeature.h"
 #include "llvm/TargetParser/Triple.h"
+#include <cassert>
+#include <memory>
 #include <optional>
+#include <system_error>
 
 using namespace llvm;
 
 #define CGOPT(TY, NAME)                                                        \
   static cl::opt<TY> *NAME##View;                                              \
   TY codegen::get##NAME() {                                                    \
-    assert(NAME##View && "RegisterCodeGenFlags not created.");                 \
+    assert(NAME##View && "Flag not registered.");                 \
     return *NAME##View;                                                        \
   }
 
 #define CGLIST(TY, NAME)                                                       \
   static cl::list<TY> *NAME##View;                                             \
   std::vector<TY> codegen::get##NAME() {                                       \
-    assert(NAME##View && "RegisterCodeGenFlags not created.");                 \
+    assert(NAME##View && "Flag not registered.");                 \
     return *NAME##View;                                                        \
   }
 
@@ -110,13 +120,14 @@ CGOPT(bool, DebugStrictDwarf)
 CGOPT(unsigned, AlignLoops)
 CGOPT(bool, JMCInstrument)
 CGOPT(bool, XCOFFReadOnlyPointers)
+CGOPT(codegen::SaveStatsMode, SaveStats)
 
-codegen::RegisterCodeGenFlags::RegisterCodeGenFlags() {
 #define CGBINDOPT(NAME)                                                        \
   do {                                                                         \
     NAME##View = std::addressof(NAME);                                         \
   } while (0)
 
+codegen::RegisterCodeGenFlags::RegisterCodeGenFlags() {
   static cl::opt<std::string> MArch(
       "march", cl::desc("Architecture to generate code for (see --version)"));
   CGBINDOPT(MArch);
@@ -515,11 +526,24 @@ codegen::RegisterCodeGenFlags::RegisterCodeGenFlags() {
       cl::init(false));
   CGBINDOPT(DisableIntegratedAS);
 
-#undef CGBINDOPT
-
   mc::RegisterMCTargetOptionsFlags();
 }
 
+codegen::RegisterSaveStatsFlag::RegisterSaveStatsFlag() {
+  static cl::opt<SaveStatsMode> SaveStats(
+    "save-stats",
+    cl::desc("Save LLVM statistics to a file in the current directory"
+             "(`-save-stats`/`-save-stats=cwd`) or the directory of the output"
+             "file (`-save-stats=obj`). (default: cwd)"),
+    cl::values(clEnumValN(SaveStatsMode::Cwd, "cwd",
+                          "Save to the current working directory"),
+               clEnumValN(SaveStatsMode::Cwd, "", ""),
+               clEnumValN(SaveStatsMode::Obj, "obj",
+                          "Save to the output file directory")),
+    cl::init(SaveStatsMode::None), cl::ValueOptional);
+    CGBINDOPT(SaveStats);
+}
+
 llvm::BasicBlockSection
 codegen::getBBSectionsMode(llvm::TargetOptions &Options) {
   if (getBBSections() == "all")
@@ -763,3 +787,43 @@ codegen::createTargetMachineForTriple(StringRef TargetTriple,
                                  TargetTriple);
   return std::unique_ptr<TargetMachine>(Target);
 }
+
+int codegen::MaybeEnableStatistics() {
+  if (getSaveStats() == SaveStatsMode::None)
+    return 0;
+
+  llvm::EnableStatistics(false);
+  return 0;
+}
+
+int codegen::MaybeSaveStatistics(StringRef OutputFilename, StringRef ToolName) {
+  auto SaveStatsValue = getSaveStats();
+  if (SaveStatsValue == codegen::SaveStatsMode::None)
+    return 0;
+
+  SmallString<128> StatsFilename;
+  if (SaveStatsValue == codegen::SaveStatsMode::Obj) {
+    StatsFilename = OutputFilename;
+    llvm::sys::path::remove_filename(StatsFilename);
+  } else {
+    assert(SaveStatsValue == codegen::SaveStatsMode::Cwd &&
+           "Should have been a valid --save-stats value");
+  }
+
+  auto BaseName = llvm::sys::path::filename(OutputFilename);
+  llvm::sys::path::append(StatsFilename, BaseName);
+  llvm::sys::path::replace_extension(StatsFilename, "stats");
+
+  auto FileFlags = llvm::sys::fs::OF_TextWithCRLF;
+  std::error_code EC;
+  auto StatsOS =
+      std::make_unique<llvm::raw_fd_ostream>(StatsFilename, EC, FileFlags);
+  if (EC) {
+    WithColor::error(errs(), ToolName)
+        << "Unable to open statistics file: " << EC.message() << "\n";
+    return 1;
+  }
+
+  llvm::PrintStatisticsJSON(*StatsOS);
+  return 0;
+}
diff --git a/llvm/test/tools/opt/save-stats.ll b/llvm/test/tools/opt/save-stats.ll
new file mode 100644
index 0000000000000..7dd0c4535c5d3
--- /dev/null
+++ b/llvm/test/tools/opt/save-stats.ll
@@ -0,0 +1,15 @@
+; REQUIRES: asserts
+
+; RUN: opt -S -passes=instcombine --save-stats=obj -o %t.ll %s && cat %t.stats | FileCheck %s
+; RUN: opt -S -passes=instcombine --save-stats=cwd -o %t.ll %s && cat %{t:stem}.tmp.stats | FileCheck %s
+; RUN: opt -S -passes=instcombine --save-stats -o %t.ll %s && cat %{t:stem}.tmp.stats | FileCheck %s
+; RUN: not opt -S --save-stats=invalid -o %t.ll %s 2>&1 | FileCheck %s --check-prefix=INVALID_ARG
+
+; CHECK: {
+; CHECK: "instcombine.NumWorklistIterations":
+; CHECK: }
+
+; INVALID_ARG: {{.*}}opt{{.*}}: for the --save-stats option: Cannot find option named 'invalid'!
+define i32 @func() {
+  ret i32 0
+}
diff --git a/llvm/tools/llc/llc.cpp b/llvm/tools/llc/llc.cpp
index dc2f878830863..cd14bf561df8d 100644
--- a/llvm/tools/llc/llc.cpp
+++ b/llvm/tools/llc/llc.cpp
@@ -65,6 +65,7 @@
 using namespace llvm;
 
 static codegen::RegisterCodeGenFlags CGF;
+static codegen::RegisterSaveStatsFlag SSF;
 
 // General options for llc.  Other pass-specific options are specified
 // within the corresponding llc passes, and target-specific options
@@ -211,20 +212,6 @@ static cl::opt<std::string> RemarksFormat(
     cl::desc("The format used for serializing remarks (default: YAML)"),
     cl::value_desc("format"), cl::init("yaml"));
 
-enum SaveStatsMode { None, Cwd, Obj };
-
-static cl::opt<SaveStatsMode> SaveStats(
-    "save-stats",
-    cl::desc("Save LLVM statistics to a file in the current directory"
-             "(`-save-stats`/`-save-stats=cwd`) or the directory of the output"
-             "file (`-save-stats=obj`). (default: cwd)"),
-    cl::values(clEnumValN(SaveStatsMode::Cwd, "cwd",
-                          "Save to the current working directory"),
-               clEnumValN(SaveStatsMode::Cwd, "", ""),
-               clEnumValN(SaveStatsMode::Obj, "obj",
-                          "Save to the output file directory")),
-    cl::init(SaveStatsMode::None), cl::ValueOptional);
-
 static cl::opt<bool> EnableNewPassManager(
     "enable-new-pm", cl::desc("Enable the new pass manager"), cl::init(false));
 
@@ -377,46 +364,6 @@ static std::unique_ptr<ToolOutputFile> GetOutputStream(const char *TargetName,
 
   return FDOut;
 }
-
-static int MaybeEnableStats() {
-  if (SaveStats == SaveStatsMode::None)
-    return 0;
-
-  llvm::EnableStatistics(false);
-  return 0;
-}
-
-static int MaybeSaveStats(std::string &&OutputFilename) {
-  if (SaveStats == SaveStatsMode::None)
-    return 0;
-
-  SmallString<128> StatsFilename;
-  if (SaveStats == SaveStatsMode::Obj) {
-    StatsFilename = OutputFilename;
-    llvm::sys::path::remove_filename(StatsFilename);
-  } else {
-    assert(SaveStats == SaveStatsMode::Cwd &&
-           "Should have been a valid --save-stats value");
-  }
-
-  auto BaseName = llvm::sys::path::filename(OutputFilename);
-  llvm::sys::path::append(StatsFilename, BaseName);
-  llvm::sys::path::replace_extension(StatsFilename, "stats");
-
-  auto FileFlags = llvm::sys::fs::OF_TextWithCRLF;
-  std::error_code EC;
-  auto StatsOS =
-      std::make_unique<llvm::raw_fd_ostream>(StatsFilename, EC, FileFlags);
-  if (EC) {
-    WithColor::error(errs(), "llc")
-        << "Unable to open statistics file: " << EC.message() << "\n";
-    return 1;
-  }
-
-  llvm::PrintStatisticsJSON(*StatsOS);
-  return 0;
-}
-
 // main - Entry point for the llc compiler.
 //
 int main(int argc, char **argv) {
@@ -494,7 +441,7 @@ int main(int argc, char **argv) {
     reportError(std::move(E), RemarksFilename);
   LLVMRemarkFileHandle RemarksFile = std::move(*RemarksFileOrErr);
 
-  if (int RetVal = MaybeEnableStats())
+  if (int RetVal = codegen::MaybeEnableStatistics())
     return RetVal;
   std::string OutputFilename;
 
@@ -510,7 +457,7 @@ int main(int argc, char **argv) {
   if (RemarksFile)
     RemarksFile->keep();
 
-  return MaybeSaveStats(std::move(OutputFilename));
+  return codegen::MaybeSaveStatistics(OutputFilename, "llc");
 }
 
 static bool addPass(PassManagerBase &PM, const char *argv0, StringRef PassName,
diff --git a/llvm/tools/opt/optdriver.cpp b/llvm/tools/opt/optdriver.cpp
index f70db3133f69d..18897959d7c78 100644
--- a/llvm/tools/opt/optdriver.cpp
+++ b/llvm/tools/opt/optdriver.cpp
@@ -64,6 +64,7 @@ using namespace llvm;
 using namespace opt_tool;
 
 static codegen::RegisterCodeGenFlags CFG;
+static codegen::RegisterSaveStatsFlag SSF;
 
 // The OptimizationList is automatically populated with registered Passes by the
 // PassNameParser.
@@ -512,6 +513,9 @@ optMain(int argc, char **argv,
   }
   LLVMRemarkFileHandle RemarksFile = std::move(*RemarksFileOrErr);
 
+  if (int RetVal = codegen::MaybeEnableStatistics())
+    return RetVal;
+
   // Load the input module...
   auto SetDataLayout = [&](StringRef IRTriple,
                            StringRef IRLayout) -> std::optional<std::string> {
@@ -742,15 +746,15 @@ optMain(int argc, char **argv,
     // The user has asked to use the new pass manager and provided a pipeline
     // string. Hand off the rest of the functionality to the new code for that
     // layer.
-    return runPassPipeline(
+    if (!runPassPipeline(
                argv[0], *M, TM.get(), &TLII, Out.get(), ThinLinkOut.get(),
                RemarksFile.get(), Pipeline, PluginList, PassBuilderCallbacks,
                OK, VK, /* ShouldPreserveAssemblyUseListOrder */ false,
                /* ShouldPreserveBitcodeUseListOrder */ true, EmitSummaryIndex,
                EmitModuleHash, EnableDebugify, VerifyDebugInfoPreserve,
-               EnableProfileVerification, UnifiedLTO)
-               ? 0
-               : 1;
+               EnableProfileVerification, UnifiedLTO))
+      return 1;
+    return codegen::MaybeSaveStatistics(OutputFilename, "opt");
   }
 
   if (OptLevelO0 || OptLevelO1 || OptLevelO2 || OptLevelOs || OptLevelOz ||
@@ -928,5 +932,5 @@ optMain(int argc, char **argv,
   if (ThinLinkOut)
     ThinLinkOut->keep();
 
-  return 0;
+  return codegen::MaybeSaveStatistics(OutputFilename, "opt");
 }



More information about the llvm-commits mailing list