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

Tomer Shafir via llvm-commits llvm-commits at lists.llvm.org
Tue Nov 11 06:19:23 PST 2025


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

>From af7657173218d874ea39c0e7e6a59f37e0f5ffd2 Mon Sep 17 00:00:00 2001
From: tomershafir <tomer.shafir8 at gmail.com>
Date: Mon, 10 Nov 2025 14:06:44 +0200
Subject: [PATCH 1/2] [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 | 21 +++++++
 llvm/lib/CodeGen/CommandFlags.cpp        | 75 ++++++++++++++++++++++--
 llvm/test/tools/llc/save-stats.ll        |  2 +
 llvm/test/tools/opt/save-stats.ll        | 18 ++++++
 llvm/tools/llc/llc.cpp                   | 59 +------------------
 llvm/tools/opt/optdriver.cpp             | 24 ++++----
 8 files changed, 135 insertions(+), 71 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..321546559038f 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,17 @@ 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..c9a502d369bbe 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/WithColor.h"
+#include "llvm/Support/raw_ostream.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,25 @@ 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 +788,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/llc/save-stats.ll b/llvm/test/tools/llc/save-stats.ll
index a5769f86648dc..8c0c30677015a 100644
--- a/llvm/test/tools/llc/save-stats.ll
+++ b/llvm/test/tools/llc/save-stats.ll
@@ -1,6 +1,8 @@
 ; REQUIRES: asserts
 
+; Ensure the test runs in a temp directory. See https://github.com/llvm/llvm-project/pull/167403#event-20848739526
 ; RUN: rm -rf %t.dir && mkdir -p %t.dir && cd %t.dir
+
 ; RUN: llc --save-stats=obj -o %t.s %s && cat %t.stats | FileCheck %s
 ; RUN: llc --save-stats=cwd -o %t.s %s && cat %{t:stem}.tmp.stats | FileCheck %s
 ; RUN: llc --save-stats -o %t.s %s && cat %{t:stem}.tmp.stats | FileCheck %s
diff --git a/llvm/test/tools/opt/save-stats.ll b/llvm/test/tools/opt/save-stats.ll
new file mode 100644
index 0000000000000..eaaa717b261c2
--- /dev/null
+++ b/llvm/test/tools/opt/save-stats.ll
@@ -0,0 +1,18 @@
+; REQUIRES: asserts
+
+; Ensure the test runs in a temp directory. See https://github.com/llvm/llvm-project/pull/167403#event-20848739526
+; RUN: rm -rf %t.dir && mkdir -p %t.dir && cd %t.dir
+
+; 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..ac86d289c80d4 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(
-               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;
+    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))
+      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");
 }

>From 6692599784acb53ee61b1eb12b6c482ba948a194 Mon Sep 17 00:00:00 2001
From: tomershafir <tomer.shafir8 at gmail.com>
Date: Tue, 11 Nov 2025 16:19:05 +0200
Subject: [PATCH 2/2] fix typos and make return type void

---
 llvm/include/llvm/CodeGen/CommandFlags.h | 12 ++++++------
 llvm/lib/CodeGen/CommandFlags.cpp        |  5 ++---
 llvm/tools/llc/llc.cpp                   |  3 +--
 llvm/tools/opt/optdriver.cpp             |  3 +--
 4 files changed, 10 insertions(+), 13 deletions(-)

diff --git a/llvm/include/llvm/CodeGen/CommandFlags.h b/llvm/include/llvm/CodeGen/CommandFlags.h
index 321546559038f..59aacc75e055d 100644
--- a/llvm/include/llvm/CodeGen/CommandFlags.h
+++ b/llvm/include/llvm/CodeGen/CommandFlags.h
@@ -214,13 +214,13 @@ LLVM_ABI Expected<std::unique_ptr<TargetMachine>> createTargetMachineForTriple(
     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();
+/// based on the value of the flag. Must be called before the tool run to
+/// actually collect data.
+LLVM_ABI void 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
+/// Conditionally saves the collected LLVM statistics to the received output
+/// file, based on the value of the flag. Should be called after the tool run,
+/// and must follow a call to `MaybeEnableStatistics()` to actually have data to
 /// write.
 LLVM_ABI int MaybeSaveStatistics(StringRef OutputFilename, StringRef ToolName);
 
diff --git a/llvm/lib/CodeGen/CommandFlags.cpp b/llvm/lib/CodeGen/CommandFlags.cpp
index c9a502d369bbe..e7d76e138bac7 100644
--- a/llvm/lib/CodeGen/CommandFlags.cpp
+++ b/llvm/lib/CodeGen/CommandFlags.cpp
@@ -789,12 +789,11 @@ codegen::createTargetMachineForTriple(StringRef TargetTriple,
   return std::unique_ptr<TargetMachine>(Target);
 }
 
-int codegen::MaybeEnableStatistics() {
+void codegen::MaybeEnableStatistics() {
   if (getSaveStats() == SaveStatsMode::None)
-    return 0;
+    return;
 
   llvm::EnableStatistics(false);
-  return 0;
 }
 
 int codegen::MaybeSaveStatistics(StringRef OutputFilename, StringRef ToolName) {
diff --git a/llvm/tools/llc/llc.cpp b/llvm/tools/llc/llc.cpp
index cd14bf561df8d..a257b351ec846 100644
--- a/llvm/tools/llc/llc.cpp
+++ b/llvm/tools/llc/llc.cpp
@@ -441,8 +441,7 @@ int main(int argc, char **argv) {
     reportError(std::move(E), RemarksFilename);
   LLVMRemarkFileHandle RemarksFile = std::move(*RemarksFileOrErr);
 
-  if (int RetVal = codegen::MaybeEnableStatistics())
-    return RetVal;
+  codegen::MaybeEnableStatistics();
   std::string OutputFilename;
 
   if (InputLanguage != "" && InputLanguage != "ir" && InputLanguage != "mir")
diff --git a/llvm/tools/opt/optdriver.cpp b/llvm/tools/opt/optdriver.cpp
index ac86d289c80d4..ef6e5412bda48 100644
--- a/llvm/tools/opt/optdriver.cpp
+++ b/llvm/tools/opt/optdriver.cpp
@@ -513,8 +513,7 @@ optMain(int argc, char **argv,
   }
   LLVMRemarkFileHandle RemarksFile = std::move(*RemarksFileOrErr);
 
-  if (int RetVal = codegen::MaybeEnableStatistics())
-    return RetVal;
+  codegen::MaybeEnableStatistics();
 
   // Load the input module...
   auto SetDataLayout = [&](StringRef IRTriple,



More information about the llvm-commits mailing list