[clang] [CopyProf] Clang frontend and driver integration. (PR #225905)
via cfe-commits
cfe-commits at lists.llvm.org
Fri Sep 25 03:47:44 PDT 2026
https://github.com/newgre updated https://github.com/llvm/llvm-project/pull/225905
>From 606319129e6cd63ac46fa3a4c24caee81ad23212 Mon Sep 17 00:00:00 2001
From: Jan Newger <jannewger at google.com>
Date: Wed, 24 Jun 2026 10:36:24 +0000
Subject: [PATCH 1/2] [CopyProf] Clang frontend and driver integration.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This wires the existing passes and runtime up to the Clang driver so CopyProf
can actually be enabled from the command line.
Adds two flags:
- `-fcopyprof` — to enable CopyProf.
- `-fcopyprof-static-size-threshold=<N>` — minimum C++ object size in bytes
to enable instrumentation.
RFC: https://discourse.llvm.org/t/rfc-copysanitizer-csan-detecting-unneccessary-object-copies-at-runtime/91038
Previous PR: #223458
---
clang/include/clang/Basic/CodeGenOptions.def | 2 +
clang/include/clang/Driver/SanitizerArgs.h | 2 +
clang/include/clang/Options/Options.td | 12 +++
clang/lib/CodeGen/BackendUtil.cpp | 18 +++++
clang/lib/CodeGen/CodeGenFunction.cpp | 32 ++++++++
clang/lib/Driver/SanitizerArgs.cpp | 3 +
clang/lib/Driver/ToolChains/Clang.cpp | 8 ++
clang/lib/Driver/ToolChains/CommonArgs.cpp | 2 +
clang/runtime/CMakeLists.txt | 2 +-
clang/test/CodeGen/copyprof.cpp | 34 ++++++++
.../CodeGen/no-skipped-passes-O0-opt-bisect.c | 2 +
clang/test/CodeGenCXX/copyprof-attrs-move.cpp | 37 +++++++++
clang/test/CodeGenCXX/copyprof-attrs.cpp | 80 +++++++++++++++++++
clang/test/Driver/fcopyprof.cpp | 34 ++++++++
14 files changed, 267 insertions(+), 1 deletion(-)
create mode 100644 clang/test/CodeGen/copyprof.cpp
create mode 100644 clang/test/CodeGenCXX/copyprof-attrs-move.cpp
create mode 100644 clang/test/CodeGenCXX/copyprof-attrs.cpp
create mode 100644 clang/test/Driver/fcopyprof.cpp
diff --git a/clang/include/clang/Basic/CodeGenOptions.def b/clang/include/clang/Basic/CodeGenOptions.def
index dd9590d406d5e1..10d9a4e3a6a82c 100644
--- a/clang/include/clang/Basic/CodeGenOptions.def
+++ b/clang/include/clang/Basic/CodeGenOptions.def
@@ -298,6 +298,8 @@ CODEGENOPT(SanitizeCfiICallNormalizeIntegers, 1, 0, Benign) ///< Normalize integ
CODEGENOPT(SanitizeCfiCanonicalJumpTables, 1, 0, Benign) ///< Make jump table symbols canonical
///< instead of creating a local jump table.
CODEGENOPT(SanitizeKcfiArity, 1, 0, Benign) ///< Embed arity in KCFI patchable function prefix
+CODEGENOPT(CopyProf, 1, 0, Benign) ///< Enable CopyProf instrumentation
+VALUE_CODEGENOPT(CopyProfStaticSizeThreshold, 32, 16, Benign) ///< Set threshold for CopyProf instrumentation
CODEGENOPT(SanitizeCoverageType, 2, 0, Benign) ///< Type of sanitizer coverage
///< instrumentation.
CODEGENOPT(SanitizeCoverageIndirectCalls, 1, 0, Benign) ///< Enable sanitizer coverage
diff --git a/clang/include/clang/Driver/SanitizerArgs.h b/clang/include/clang/Driver/SanitizerArgs.h
index 62f154bae3ffd3..d7823d71b58601 100644
--- a/clang/include/clang/Driver/SanitizerArgs.h
+++ b/clang/include/clang/Driver/SanitizerArgs.h
@@ -78,6 +78,7 @@ class SanitizerArgs {
// True if cross-dso CFI support if provided by the system (i.e. Android).
bool ImplicitCfiRuntime = false;
bool NeedsMemProfRt = false;
+ bool NeedsCopyProfRt = false;
bool HwasanUseAliases = false;
llvm::AsanDetectStackUseAfterReturnMode AsanUseAfterReturn =
llvm::AsanDetectStackUseAfterReturnMode::Invalid;
@@ -113,6 +114,7 @@ class SanitizerArgs {
!Sanitizers.has(SanitizerKind::Address) &&
!Sanitizers.has(SanitizerKind::HWAddress);
}
+ bool needsCopyProfRt() const { return NeedsCopyProfRt; }
bool needsFuzzerInterceptors() const;
bool needsUbsanRt() const;
bool needsUbsanCXXRt() const;
diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td
index 15b2196b68e76e..1bf7092e37d3d1 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -2662,6 +2662,18 @@ def fmemory_profile_use_EQ : Joined<["-"], "fmemory-profile-use=">,
HelpText<"Use memory profile for profile-guided memory optimization">,
MarshallingInfoString<CodeGenOpts<"MemoryProfileUsePath">>;
+defm copyprof : BoolFOption<"copyprof",
+ CodeGenOpts<"CopyProf">, DefaultFalse,
+ PosFlag<SetTrue, [], [ClangOption, CC1Option], "Enable">,
+ NegFlag<SetFalse, [], [ClangOption], "Disable">,
+ BothFlags<[], [ClangOption], " copy profiling">>;
+def fcopyprof_static_size_threshold_EQ
+ : Joined<["-"], "fcopyprof-static-size-threshold=">,
+ Group<f_Group>, Visibility<[ClangOption, CC1Option]>,
+ MetaVarName<"<N>">,
+ HelpText<"Minimum C++ object size to enable CopyProf instrumentation (default: 16).">,
+ MarshallingInfoInt<CodeGenOpts<"CopyProfStaticSizeThreshold">, "16">;
+
// Begin sanitizer flags. These should all be core options exposed in all driver
// modes.
let Visibility = [ClangOption, CC1Option, CLOption] in {
diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp
index 8103b72cf4e3a2..3a02922d592beb 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -74,6 +74,7 @@
#include "llvm/Transforms/Instrumentation/AddressSanitizer.h"
#include "llvm/Transforms/Instrumentation/AddressSanitizerOptions.h"
#include "llvm/Transforms/Instrumentation/BoundsChecking.h"
+#include "llvm/Transforms/Instrumentation/CopyProf.h"
#include "llvm/Transforms/Instrumentation/DataFlowSanitizer.h"
#include "llvm/Transforms/Instrumentation/GCOVProfiler.h"
#include "llvm/Transforms/Instrumentation/HWAddressSanitizer.h"
@@ -1059,6 +1060,23 @@ void EmitAssemblyHelper::RunOptimizationPipeline(
MPM.addPass(InferFunctionAttrsPass());
}
});
+
+ if (CodeGenOpts.CopyProf) {
+ // Early pass: insert callbacks into special member functions before the
+ // inliner removes function boundaries.
+ PB.registerPipelineEarlySimplificationEPCallback(
+ [](ModulePassManager &MPM, OptimizationLevel, ThinOrFullLTOPhase) {
+ MPM.addPass(createModuleToFunctionPassAdaptor(CopyProfPass()));
+ MPM.addPass(ModuleCopyProfPass());
+ });
+ // Late pass: to reduce runtime overhead, instrument stores only after
+ // optimizations have been run so only useful stores are instrumented.
+ PB.registerOptimizerLastEPCallback([](ModulePassManager &MPM,
+ OptimizationLevel,
+ ThinOrFullLTOPhase) {
+ MPM.addPass(createModuleToFunctionPassAdaptor(CopyProfStoresPass()));
+ });
+ }
}
if (std::optional<GCOVOptions> Options =
diff --git a/clang/lib/CodeGen/CodeGenFunction.cpp b/clang/lib/CodeGen/CodeGenFunction.cpp
index 745423945ca838..effd7283854205 100644
--- a/clang/lib/CodeGen/CodeGenFunction.cpp
+++ b/clang/lib/CodeGen/CodeGenFunction.cpp
@@ -40,6 +40,7 @@
#include "clang/CodeGenUtils/FunctionUtils.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/ScopeExit.h"
+#include "llvm/ADT/StringExtras.h"
#include "llvm/Frontend/OpenMP/OMPIRBuilder.h"
#include "llvm/IR/DataLayout.h"
#include "llvm/IR/Dominators.h"
@@ -1053,6 +1054,37 @@ void CodeGenFunction::StartFunction(GlobalDecl GD, QualType RetTy,
}
}
+ // Annotate C++ special member functions so that CopyProfPass can instrument
+ // them, provided the object is at least as large as the size threshold.
+ if (CGM.getCodeGenOpts().CopyProf) {
+ const auto *MD = dyn_cast_or_null<CXXMethodDecl>(D);
+ const auto *CD = MD ? dyn_cast<CXXConstructorDecl>(MD) : nullptr;
+ // It's either some sort of c'tor (but not any move function), or either of
+ // copy assignment operator / d'tor.
+ bool IsCandidate =
+ CD ? !(CD->isMoveConstructor() || CD->isMoveAssignmentOperator())
+ : MD &&
+ (isa<CXXDestructorDecl>(MD) || MD->isCopyAssignmentOperator());
+ if (IsCandidate) {
+ // A special member function always has an implicit object parameter, so
+ // its type is guaranteed to be complete here.
+ CharUnits ObjSize =
+ getContext().getTypeSizeInChars(MD->getFunctionObjectParameterType());
+ if (ObjSize.getQuantity() >=
+ CGM.getCodeGenOpts().CopyProfStaticSizeThreshold) {
+ std::string ObjSizeStr = llvm::utostr(ObjSize.getQuantity());
+ if (CD)
+ Fn->addFnAttr(CD->isCopyConstructor() ? "copyprof-copy-ctor"
+ : "copyprof-ctor",
+ ObjSizeStr);
+ else if (isa<CXXDestructorDecl>(MD))
+ Fn->addFnAttr("copyprof-dtor", ObjSizeStr);
+ else
+ Fn->addFnAttr("copyprof-copy-assign-op", ObjSizeStr);
+ }
+ }
+ }
+
// If we're in C++ mode and the function name is "main", it is guaranteed
// to be norecurse by the standard (3.6.1.3 "The function main shall not be
// used within a program").
diff --git a/clang/lib/Driver/SanitizerArgs.cpp b/clang/lib/Driver/SanitizerArgs.cpp
index 778cde8285aaf1..438283c60c0ba2 100644
--- a/clang/lib/Driver/SanitizerArgs.cpp
+++ b/clang/lib/Driver/SanitizerArgs.cpp
@@ -1345,6 +1345,9 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC,
options::OPT_fmemory_profile_EQ,
options::OPT_fno_memory_profile, false);
+ NeedsCopyProfRt =
+ Args.hasFlag(options::OPT_fcopyprof, options::OPT_fno_copyprof, false);
+
// Finally, initialize the set of available and recoverable sanitizers.
Sanitizers.Mask |= Kinds;
RecoverableSanitizers.Mask |= RecoverableKinds;
diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp
index 6636a5fd6e6551..1b13be2aca3b9f 100644
--- a/clang/lib/Driver/ToolChains/Clang.cpp
+++ b/clang/lib/Driver/ToolChains/Clang.cpp
@@ -5645,6 +5645,14 @@ void Clang::ConstructJob(Compilation &C, const JobAction &JA,
MemProfUseArg->render(Args, CmdArgs);
}
+ auto *CopyProfArg =
+ Args.getLastArg(options::OPT_fcopyprof, options::OPT_fno_copyprof);
+ if (CopyProfArg &&
+ !CopyProfArg->getOption().matches(options::OPT_fno_copyprof)) {
+ CopyProfArg->render(Args, CmdArgs);
+ Args.AddLastArg(CmdArgs, options::OPT_fcopyprof_static_size_threshold_EQ);
+ }
+
// Embed-bitcode option.
// Only white-listed flags below are allowed to be embedded.
if (C.getDriver().embedBitcodeInObject() && !IsUsingLTO &&
diff --git a/clang/lib/Driver/ToolChains/CommonArgs.cpp b/clang/lib/Driver/ToolChains/CommonArgs.cpp
index 445eb4ccfbfa72..a2d508b34c5fc0 100644
--- a/clang/lib/Driver/ToolChains/CommonArgs.cpp
+++ b/clang/lib/Driver/ToolChains/CommonArgs.cpp
@@ -1792,6 +1792,8 @@ collectSanitizerRuntimes(Compilation &C, const ToolChain &TC,
StaticRuntimes.push_back("dfsan");
if (SanArgs.needsLsanRt())
StaticRuntimes.push_back("lsan");
+ if (SanArgs.needsCopyProfRt())
+ StaticRuntimes.push_back("copyprof");
if (SanArgs.needsMsanRt()) {
StaticRuntimes.push_back("msan");
if (SanArgs.linkCXXRuntimes())
diff --git a/clang/runtime/CMakeLists.txt b/clang/runtime/CMakeLists.txt
index ff2605b23d25b0..f543bba2f27f30 100644
--- a/clang/runtime/CMakeLists.txt
+++ b/clang/runtime/CMakeLists.txt
@@ -122,7 +122,7 @@ if(LLVM_BUILD_EXTERNAL_COMPILER_RT AND EXISTS ${COMPILER_RT_SRC_ROOT}/)
COMPONENT compiler-rt)
# Add top-level targets that build specific compiler-rt runtimes.
- set(COMPILER_RT_RUNTIMES fuzzer asan builtins dfsan lsan msan profile tsan tysan ubsan ubsan-minimal)
+ set(COMPILER_RT_RUNTIMES fuzzer asan builtins copyprof dfsan lsan msan profile tsan tysan ubsan ubsan-minimal)
foreach(runtime ${COMPILER_RT_RUNTIMES})
get_ext_project_build_command(build_runtime_cmd ${runtime})
add_custom_target(${runtime}
diff --git a/clang/test/CodeGen/copyprof.cpp b/clang/test/CodeGen/copyprof.cpp
new file mode 100644
index 00000000000000..3c06b14e6df772
--- /dev/null
+++ b/clang/test/CodeGen/copyprof.cpp
@@ -0,0 +1,34 @@
+// Test that CopyProf instrumentation passes are invoked at -O0 and -O2,
+// and are not re-run during ThinLTO postlink backend compilation.
+
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O0 -fcopyprof %s -fdebug-pass-manager -emit-llvm -o /dev/null 2>&1 | FileCheck %s --check-prefix=INSTRUMENT
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O2 -fcopyprof %s -fdebug-pass-manager -emit-llvm -o /dev/null 2>&1 | FileCheck %s --check-prefix=INSTRUMENT
+
+// INSTRUMENT: Running pass: CopyProfPass on
+// INSTRUMENT: Running pass: ModuleCopyProfPass on [module]
+// INSTRUMENT: Running pass: CopyProfStoresPass on
+
+// Test ThinLTO prelink vs postlink:
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O2 -fcopyprof -flto=thin -emit-llvm-bc %s -o %t.bc
+// RUN: llvm-lto -thinlto -o %t %t.bc
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O2 -fcopyprof -fthinlto-index=%t.thinlto.bc -fdebug-pass-manager -emit-obj -x ir %t.bc -o /dev/null 2>&1 | FileCheck %s --check-prefix=POSTLINK
+
+// POSTLINK-NOT: Running pass: CopyProfPass
+// POSTLINK-NOT: Running pass: ModuleCopyProfPass
+// POSTLINK-NOT: Running pass: CopyProfStoresPass
+
+struct Foo {
+ long a[2];
+ Foo() : a{0, 0} {}
+ Foo(const Foo &other) {
+ a[0] = other.a[0];
+ a[1] = other.a[1];
+ }
+ ~Foo() {}
+};
+
+int main() {
+ Foo f1;
+ Foo f2(f1);
+ return 0;
+}
diff --git a/clang/test/CodeGen/no-skipped-passes-O0-opt-bisect.c b/clang/test/CodeGen/no-skipped-passes-O0-opt-bisect.c
index 6d3e6359d37c77..5ed2ec377c96c0 100644
--- a/clang/test/CodeGen/no-skipped-passes-O0-opt-bisect.c
+++ b/clang/test/CodeGen/no-skipped-passes-O0-opt-bisect.c
@@ -11,6 +11,7 @@
// RUN: %clang_cc1 -triple x86_64-linux-gnu -O0 %s -fdebug-pass-manager -emit-llvm -o /dev/null -fsanitize-coverage-trace-pc-guard 2>&1 | FileCheck %s
// RUN: %clang_cc1 -triple x86_64-linux-gnu -O0 %s -fdebug-pass-manager -emit-llvm -o /dev/null -fsanitize=numerical 2>&1 | FileCheck %s
// RUN: %clang_cc1 -triple x86_64-linux-gnu -O0 %s -fdebug-pass-manager -emit-llvm -o /dev/null -fmemory-profile 2>&1 | FileCheck %s
+// RUN: %clang_cc1 -triple x86_64-linux-gnu -O0 %s -fdebug-pass-manager -emit-llvm -o /dev/null -fcopyprof 2>&1 | FileCheck %s
// RUN: %clang_cc1 -triple x86_64-linux-gnu -mllvm -opt-bisect-limit=0 %s -fdebug-pass-manager -emit-llvm -o /dev/null 2>&1 | FileCheck %s
// RUN: %clang_cc1 -triple x86_64-linux-gnu -mllvm -opt-bisect-limit=0 %s -fdebug-pass-manager -emit-llvm -o /dev/null -fcoroutines 2>&1 | FileCheck %s
@@ -22,6 +23,7 @@
// RUN: %clang_cc1 -triple x86_64-linux-gnu -mllvm -opt-bisect-limit=0 %s -fdebug-pass-manager -emit-llvm -o /dev/null -fsanitize=dataflow 2>&1 | FileCheck %s
// RUN: %clang_cc1 -triple x86_64-linux-gnu -mllvm -opt-bisect-limit=0 %s -fdebug-pass-manager -emit-llvm -o /dev/null -fsanitize-coverage-trace-pc-guard 2>&1 | FileCheck %s
// RUN: %clang_cc1 -triple x86_64-linux-gnu -mllvm -opt-bisect-limit=0 %s -fdebug-pass-manager -emit-llvm -o /dev/null -fmemory-profile 2>&1 | FileCheck %s
+// RUN: %clang_cc1 -triple x86_64-linux-gnu -mllvm -opt-bisect-limit=0 %s -fdebug-pass-manager -emit-llvm -o /dev/null -fcopyprof 2>&1 | FileCheck %s
// CHECK-NOT: Skipping pass
diff --git a/clang/test/CodeGenCXX/copyprof-attrs-move.cpp b/clang/test/CodeGenCXX/copyprof-attrs-move.cpp
new file mode 100644
index 00000000000000..998a4f32ca59f5
--- /dev/null
+++ b/clang/test/CodeGenCXX/copyprof-attrs-move.cpp
@@ -0,0 +1,37 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -disable-llvm-passes -fcopyprof -fcopyprof-static-size-threshold=32 %s -o - | FileCheck %s
+
+// Assert that move constructors and move assignment operators are never annotated by CopyProf.
+
+struct S {
+ long a[4];
+ S();
+ S(const S &);
+ S(S &&);
+ S &operator=(const S &);
+ S &operator=(S &&);
+ ~S();
+};
+static_assert(sizeof(S) == 32);
+
+S::S() {}
+S::S(const S &) {}
+S::S(S &&) {}
+S &S::operator=(const S &) { return *this; }
+S &S::operator=(S &&) { return *this; }
+S::~S() {}
+
+// CHECK: define {{.*}} @_ZN1SC1Ev({{.*}}#[[CTOR:[0-9]+]]
+// CHECK: define {{.*}} @_ZN1SC1ERKS_({{.*}}#[[COPY_CTOR:[0-9]+]]
+// CHECK: define {{.*}} @_ZN1SC1EOS_({{.*}}#[[MOVE:[0-9]+]]
+// CHECK: define {{.*}} @_ZN1SaSERKS_({{.*}}#[[COPY_ASSIGN:[0-9]+]]
+// CHECK: define {{.*}} @_ZN1SaSEOS_({{.*}}#[[MOVE]]
+// CHECK: define {{.*}} @_ZN1SD1Ev({{.*}}#[[DTOR:[0-9]+]]
+
+// CHECK: attributes #[[CTOR]] = {{{.*}}"copyprof-ctor"="32"{{.*}}}
+// CHECK: attributes #[[COPY_CTOR]] = {{{.*}}"copyprof-copy-ctor"="32"{{.*}}}
+// The move constructor and the move assignment operator share this attribute
+// group, and it must not carry any CopyProf annotation.
+// CHECK: attributes #[[MOVE]] =
+// CHECK-NOT: "copyprof-
+// CHECK: attributes #[[COPY_ASSIGN]] = {{{.*}}"copyprof-copy-assign-op"="32"{{.*}}}
+// CHECK: attributes #[[DTOR]] = {{{.*}}"copyprof-dtor"="32"{{.*}}}
diff --git a/clang/test/CodeGenCXX/copyprof-attrs.cpp b/clang/test/CodeGenCXX/copyprof-attrs.cpp
new file mode 100644
index 00000000000000..b2f569036b3436
--- /dev/null
+++ b/clang/test/CodeGenCXX/copyprof-attrs.cpp
@@ -0,0 +1,80 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -disable-llvm-passes -fcopyprof -fcopyprof-static-size-threshold=16 %s -o - | FileCheck %s --check-prefix=T16
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -disable-llvm-passes -fcopyprof -fcopyprof-static-size-threshold=8 %s -o - | FileCheck %s --check-prefix=T8
+
+// Asserts that special member functions are annotated depending on whether the object size is above the static size threshold.
+
+struct Small {
+ long a;
+ Small() : a(0) {}
+ Small(const Small &other) : a(other.a) {}
+ Small &operator=(const Small &other) {
+ a = other.a;
+ return *this;
+ }
+ ~Small() {}
+};
+static_assert(sizeof(Small) == 8);
+
+struct Large {
+ long a[2];
+ Large() : a{0, 0} {}
+ Large(const Large &other) {
+ a[0] = other.a[0];
+ a[1] = other.a[1];
+ }
+ Large &operator=(const Large &other) {
+ a[0] = other.a[0];
+ a[1] = other.a[1];
+ return *this;
+ }
+ ~Large() {}
+};
+static_assert(sizeof(Large) == 16);
+
+void test() {
+ Small s1;
+ Small s2(s1);
+ s1 = s2;
+
+ Large l1;
+ Large l2(l1);
+ l1 = l2;
+}
+
+// Threshold 16: `Small` must not be annotated but `Large` must be.
+
+// T16: define {{.*}} @_ZN5SmallC1Ev({{.*}}#[[SMALL:[0-9]+]]
+// T16: define {{.*}} @_ZN5SmallC1ERKS_({{.*}}#[[SMALL]]
+// T16: define {{.*}} @_ZN5SmallaSERKS_({{.*}}#[[SMALL]]
+// T16: define {{.*}} @_ZN5LargeC1Ev({{.*}}#[[LARGE_CTOR:[0-9]+]]
+// T16: define {{.*}} @_ZN5LargeC1ERKS_({{.*}}#[[LARGE_COPY_CTOR:[0-9]+]]
+// T16: define {{.*}} @_ZN5LargeaSERKS_({{.*}}#[[LARGE_ASSIGN:[0-9]+]]
+// T16: define {{.*}} @_ZN5LargeD1Ev({{.*}}#[[LARGE_DTOR:[0-9]+]]
+// T16: define {{.*}} @_ZN5SmallD1Ev({{.*}}#[[SMALL]]
+
+// T16: attributes #[[SMALL]] =
+// T16-NOT: "copyprof-
+// T16: attributes #[[LARGE_CTOR]] = {{{.*}}"copyprof-ctor"="16"{{.*}}}
+// T16: attributes #[[LARGE_COPY_CTOR]] = {{{.*}}"copyprof-copy-ctor"="16"{{.*}}}
+// T16: attributes #[[LARGE_ASSIGN]] = {{{.*}}"copyprof-copy-assign-op"="16"{{.*}}}
+// T16: attributes #[[LARGE_DTOR]] = {{{.*}}"copyprof-dtor"="16"{{.*}}}
+
+// Threshold 8: both structs are annotated.
+
+// T8: define {{.*}} @_ZN5SmallC1Ev({{.*}}#[[SMALL_CTOR:[0-9]+]]
+// T8: define {{.*}} @_ZN5SmallC1ERKS_({{.*}}#[[SMALL_COPY_CTOR:[0-9]+]]
+// T8: define {{.*}} @_ZN5SmallaSERKS_({{.*}}#[[SMALL_ASSIGN:[0-9]+]]
+// T8: define {{.*}} @_ZN5LargeC1Ev({{.*}}#[[LARGE_CTOR8:[0-9]+]]
+// T8: define {{.*}} @_ZN5LargeC1ERKS_({{.*}}#[[LARGE_COPY_CTOR8:[0-9]+]]
+// T8: define {{.*}} @_ZN5LargeaSERKS_({{.*}}#[[LARGE_ASSIGN8:[0-9]+]]
+// T8: define {{.*}} @_ZN5LargeD1Ev({{.*}}#[[LARGE_DTOR8:[0-9]+]]
+// T8: define {{.*}} @_ZN5SmallD1Ev({{.*}}#[[SMALL_DTOR:[0-9]+]]
+
+// T8: attributes #[[SMALL_CTOR]] = {{{.*}}"copyprof-ctor"="8"{{.*}}}
+// T8: attributes #[[SMALL_COPY_CTOR]] = {{{.*}}"copyprof-copy-ctor"="8"{{.*}}}
+// T8: attributes #[[SMALL_ASSIGN]] = {{{.*}}"copyprof-copy-assign-op"="8"{{.*}}}
+// T8: attributes #[[LARGE_CTOR8]] = {{{.*}}"copyprof-ctor"="16"{{.*}}}
+// T8: attributes #[[LARGE_COPY_CTOR8]] = {{{.*}}"copyprof-copy-ctor"="16"{{.*}}}
+// T8: attributes #[[LARGE_ASSIGN8]] = {{{.*}}"copyprof-copy-assign-op"="16"{{.*}}}
+// T8: attributes #[[LARGE_DTOR8]] = {{{.*}}"copyprof-dtor"="16"{{.*}}}
+// T8: attributes #[[SMALL_DTOR]] = {{{.*}}"copyprof-dtor"="8"{{.*}}}
diff --git a/clang/test/Driver/fcopyprof.cpp b/clang/test/Driver/fcopyprof.cpp
new file mode 100644
index 00000000000000..2ac70d3349116e
--- /dev/null
+++ b/clang/test/Driver/fcopyprof.cpp
@@ -0,0 +1,34 @@
+// Test driver flags for CopyProf instrumentation and runtime linking.
+
+// Basic C and C++ invocations:
+// RUN: %clang --target=x86_64-linux-gnu -fcopyprof %s -### 2>&1 | FileCheck %s
+// RUN: %clangxx --target=x86_64-linux-gnu -fcopyprof %s -### 2>&1 | FileCheck %s
+// CHECK: "-cc1" {{.*}} "-fcopyprof"
+// CHECK: ld{{.*}}libclang_rt.copyprof
+
+// Re-enabling after -fno-copyprof:
+// RUN: %clangxx --target=x86_64-linux-gnu -fcopyprof -fno-copyprof -fcopyprof %s -### 2>&1 | FileCheck %s
+
+// Custom static size threshold (and last-flag-wins behavior):
+// RUN: %clangxx --target=x86_64-linux-gnu -fcopyprof -fcopyprof-static-size-threshold=32 %s -### 2>&1 | FileCheck %s --check-prefix=THRESHOLD32
+// THRESHOLD32: "-cc1" {{.*}} "-fcopyprof" "-fcopyprof-static-size-threshold=32"
+// THRESHOLD32: ld{{.*}}libclang_rt.copyprof
+
+// RUN: %clangxx --target=x86_64-linux-gnu -fcopyprof -fcopyprof-static-size-threshold=32 -fcopyprof-static-size-threshold=64 %s -### 2>&1 | FileCheck %s --check-prefix=THRESHOLD64
+// THRESHOLD64: "-cc1" {{.*}} "-fcopyprof" "-fcopyprof-static-size-threshold=64"
+// THRESHOLD64-NOT: "-fcopyprof-static-size-threshold=32"
+
+// Disabling CopyProf via -fno-copyprof (ensuring threshold is also suppressed):
+// RUN: %clangxx --target=x86_64-linux-gnu -fcopyprof -fno-copyprof %s -### 2>&1 | FileCheck %s --check-prefix=OFF
+// RUN: %clangxx --target=x86_64-linux-gnu -fcopyprof -fcopyprof-static-size-threshold=32 -fno-copyprof %s -### 2>&1 | FileCheck %s --check-prefix=OFF
+// RUN: %clangxx --target=x86_64-linux-gnu -fcopyprof-static-size-threshold=32 %s -### 2>&1 | FileCheck %s --check-prefix=OFF
+// OFF-NOT: "-fcopyprof"
+// OFF-NOT: "-fcopyprof-static-size-threshold"
+// OFF-NOT: libclang_rt.copyprof
+
+// Shared libraries (-shared) and relocatable links (-r) compile with -fcopyprof
+// but must not link the static CopyProf runtime archive:
+// RUN: %clangxx --target=x86_64-linux-gnu -fcopyprof -shared %s -### 2>&1 | FileCheck %s --check-prefix=NO-STATIC-RT
+// RUN: %clangxx --target=x86_64-linux-gnu -fcopyprof -r %s -### 2>&1 | FileCheck %s --check-prefix=NO-STATIC-RT
+// NO-STATIC-RT: "-cc1" {{.*}} "-fcopyprof"
+// NO-STATIC-RT-NOT: libclang_rt.copyprof
>From e8cb78e0acdacad0f20a2881e92ca5366b00a793 Mon Sep 17 00:00:00 2001
From: Jan Newger <jannewger at gmail.com>
Date: Fri, 25 Sep 2026 10:45:56 +0000
Subject: [PATCH 2/2] fixup! [CopyProf] Clang frontend and driver integration.
---
clang/include/clang/Options/Options.td | 2 +-
clang/lib/CodeGen/CodeGenFunction.cpp | 44 ++++++++++++--------------
clang/test/CodeGen/copyprof.cpp | 1 +
3 files changed, 22 insertions(+), 25 deletions(-)
diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td
index 1bf7092e37d3d1..457c55630a3715 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -2671,7 +2671,7 @@ def fcopyprof_static_size_threshold_EQ
: Joined<["-"], "fcopyprof-static-size-threshold=">,
Group<f_Group>, Visibility<[ClangOption, CC1Option]>,
MetaVarName<"<N>">,
- HelpText<"Minimum C++ object size to enable CopyProf instrumentation (default: 16).">,
+ HelpText<"Minimum C++ object size in bytes to enable CopyProf instrumentation (default: 16).">,
MarshallingInfoInt<CodeGenOpts<"CopyProfStaticSizeThreshold">, "16">;
// Begin sanitizer flags. These should all be core options exposed in all driver
diff --git a/clang/lib/CodeGen/CodeGenFunction.cpp b/clang/lib/CodeGen/CodeGenFunction.cpp
index effd7283854205..b524021b44b814 100644
--- a/clang/lib/CodeGen/CodeGenFunction.cpp
+++ b/clang/lib/CodeGen/CodeGenFunction.cpp
@@ -1057,30 +1057,26 @@ void CodeGenFunction::StartFunction(GlobalDecl GD, QualType RetTy,
// Annotate C++ special member functions so that CopyProfPass can instrument
// them, provided the object is at least as large as the size threshold.
if (CGM.getCodeGenOpts().CopyProf) {
- const auto *MD = dyn_cast_or_null<CXXMethodDecl>(D);
- const auto *CD = MD ? dyn_cast<CXXConstructorDecl>(MD) : nullptr;
- // It's either some sort of c'tor (but not any move function), or either of
- // copy assignment operator / d'tor.
- bool IsCandidate =
- CD ? !(CD->isMoveConstructor() || CD->isMoveAssignmentOperator())
- : MD &&
- (isa<CXXDestructorDecl>(MD) || MD->isCopyAssignmentOperator());
- if (IsCandidate) {
- // A special member function always has an implicit object parameter, so
- // its type is guaranteed to be complete here.
- CharUnits ObjSize =
- getContext().getTypeSizeInChars(MD->getFunctionObjectParameterType());
- if (ObjSize.getQuantity() >=
- CGM.getCodeGenOpts().CopyProfStaticSizeThreshold) {
- std::string ObjSizeStr = llvm::utostr(ObjSize.getQuantity());
- if (CD)
- Fn->addFnAttr(CD->isCopyConstructor() ? "copyprof-copy-ctor"
- : "copyprof-ctor",
- ObjSizeStr);
- else if (isa<CXXDestructorDecl>(MD))
- Fn->addFnAttr("copyprof-dtor", ObjSizeStr);
- else
- Fn->addFnAttr("copyprof-copy-assign-op", ObjSizeStr);
+ if (const auto *MD = dyn_cast_or_null<CXXMethodDecl>(D)) {
+ StringRef Attr;
+ if (const auto *CD = dyn_cast<CXXConstructorDecl>(MD)) {
+ if (!CD->isMoveConstructor())
+ Attr =
+ CD->isCopyConstructor() ? "copyprof-copy-ctor" : "copyprof-ctor";
+ } else if (isa<CXXDestructorDecl>(MD)) {
+ Attr = "copyprof-dtor";
+ } else if (MD->isCopyAssignmentOperator()) {
+ Attr = "copyprof-copy-assign-op";
+ }
+ if (!Attr.empty()) {
+ // Finally, add the object size in bytes to the annotation.
+ // A special member function always has an implicit object parameter, so
+ // its type is guaranteed to be complete here.
+ CharUnits ObjSize = getContext().getTypeSizeInChars(
+ MD->getFunctionObjectParameterType());
+ if (ObjSize.getQuantity() >=
+ CGM.getCodeGenOpts().CopyProfStaticSizeThreshold)
+ Fn->addFnAttr(Attr, llvm::utostr(ObjSize.getQuantity()));
}
}
}
diff --git a/clang/test/CodeGen/copyprof.cpp b/clang/test/CodeGen/copyprof.cpp
index 3c06b14e6df772..926db01effac39 100644
--- a/clang/test/CodeGen/copyprof.cpp
+++ b/clang/test/CodeGen/copyprof.cpp
@@ -1,6 +1,7 @@
// Test that CopyProf instrumentation passes are invoked at -O0 and -O2,
// and are not re-run during ThinLTO postlink backend compilation.
+// REQUIRES: x86-registered-target
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O0 -fcopyprof %s -fdebug-pass-manager -emit-llvm -o /dev/null 2>&1 | FileCheck %s --check-prefix=INSTRUMENT
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O2 -fcopyprof %s -fdebug-pass-manager -emit-llvm -o /dev/null 2>&1 | FileCheck %s --check-prefix=INSTRUMENT
More information about the cfe-commits
mailing list