[llvm] [TSan] Add dominance-based redundant instrumentation elimination (PR #169897)
Alexey Paznikov via llvm-commits
llvm-commits at lists.llvm.org
Wed Jun 10 23:59:41 PDT 2026
https://github.com/apaznikov updated https://github.com/llvm/llvm-project/pull/169897
>From b942bd52b747de831b85817684e0071576dd2c4a Mon Sep 17 00:00:00 2001
From: Alexey Paznikov <apaznikov at gmail.com>
Date: Wed, 3 Jun 2026 16:25:27 +0800
Subject: [PATCH] [TSan] Add dominance-based redundant instrumentation
elimination
Adds DominanceBasedElimination, a new optimization that removes redundant
TSan instrumentation when one memory access dominates another to the same
memory location with no synchronization on any CFG path between them.
If access A dominates access B, both target the same memory location
(MustAlias, with A covering at least as many bytes as B), and no
synchronization (atomic, fence, or call without nosync) exists on any
CFG path from A to B:
- A write covers both read and write races; B is redundant regardless of
whether B is a read or a write.
- A read covers only read races; B is redundant only if B is also a read.
In our research prototype (which uses a more aggressive, inter-procedural
version of this analysis), we observed the following speedups from
dominance-based elimination alone:
* Redis: 1.35x, FFmpeg: 1.30x, SQLite: 1.21x
* MySQL Select: 1.15x, MySQL Write-only: 1.11x, Memcached: 1.03x
* Selected Chromium benchmarks: Speedometer 3.1 1.06x, Parser: HTML5
Render 1.19x, SVG: SvgCubics 1.20x, Paint: Transform Changes 1.35x,
Image Decoder: WebP 1.34x.
This patch implements a conservative, intra-procedural version; absolute
speedups may be lower than the prototype. Post-dominance elimination will
follow in a subsequent patch.
The optimization is enabled by default. Caching is intentionally omitted
to keep the implementation reviewable; it can be added as a follow-up if
compile-time profiling justifies it.
This work is part of a broader research effort on optimizing dynamic
race detectors. Preprint: https://arxiv.org/abs/2512.05555
Co-authored-by: Alexey Paznikov, Andrey Kogutenko, Yaroslav Osipov,
Michael Schwarz, and Umang Mathur.
---
.../Instrumentation/ThreadSanitizer.cpp | 389 ++++++++-
.../ThreadSanitizer/capture-no-omit.ll | 2 +-
.../ThreadSanitizer/dominance-elimination.ll | 789 ++++++++++++++++++
.../ThreadSanitizer/read_before_write.ll | 8 +-
4 files changed, 1158 insertions(+), 30 deletions(-)
create mode 100644 llvm/test/Instrumentation/ThreadSanitizer/dominance-elimination.ll
diff --git a/llvm/lib/Transforms/Instrumentation/ThreadSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/ThreadSanitizer.cpp
index f05efd863fb74..3df0c0ff7b16d 100644
--- a/llvm/lib/Transforms/Instrumentation/ThreadSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/ThreadSanitizer.cpp
@@ -24,10 +24,12 @@
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/Statistic.h"
#include "llvm/ADT/StringExtras.h"
+#include "llvm/Analysis/AliasAnalysis.h"
#include "llvm/Analysis/CaptureTracking.h"
#include "llvm/Analysis/TargetLibraryInfo.h"
#include "llvm/Analysis/ValueTracking.h"
#include "llvm/IR/DataLayout.h"
+#include "llvm/IR/Dominators.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/Instructions.h"
@@ -84,6 +86,22 @@ static cl::opt<bool>
ClOmitNonCaptured("tsan-omit-by-pointer-capturing", cl::init(true),
cl::desc("Omit accesses due to pointer capturing"),
cl::Hidden);
+// Removes redundant TSan instrumentation using CFG dominance analysis.
+// If access A dominates access B, both target the same memory location
+// (confirmed via MustAlias), A covers at least as many bytes as B, and no
+// synchronization (atomic, fence, or call without nosync) exists on any CFG
+// path from A to B, then B's instrumentation is redundant: any race
+// detectable at B would already be detected at A.
+// Coverage rule: a dominating write subsumes a subsequent read or write; a
+// dominating read subsumes only a subsequent read.
+// Post-dominance elimination (removing A when B post-dominates A) will be
+// added in a follow-up patch. See DominanceBasedElimination for the full
+// correctness argument.
+static cl::opt<bool>
+ ClUseDominanceAnalysis("tsan-use-dominance-analysis", cl::init(true),
+ cl::desc("Eliminate redundant instrumentation using "
+ "dominance analysis"),
+ cl::Hidden);
STATISTIC(NumInstrumentedReads, "Number of instrumented reads");
STATISTIC(NumInstrumentedWrites, "Number of instrumented writes");
@@ -96,11 +114,86 @@ STATISTIC(NumOmittedReadsFromConstantGlobals,
"Number of reads from constant globals");
STATISTIC(NumOmittedReadsFromVtable, "Number of vtable reads");
STATISTIC(NumOmittedNonCaptured, "Number of accesses ignored due to capturing");
+STATISTIC(NumOmittedByDominance, "Number of accesses ignored due to dominance");
const char kTsanModuleCtorName[] = "tsan.module_ctor";
const char kTsanInitName[] = "__tsan_init";
namespace {
+// Internal Instruction wrapper that contains more information about the
+// Instruction from prior analysis.
+struct InstructionInfo {
+ // Instrumentation emitted for this instruction is for a compounded set of
+ // read and write operations in the same basic block.
+ static constexpr unsigned kCompoundRW = (1U << 0);
+
+ explicit InstructionInfo(Instruction *Inst) : Inst(Inst) {}
+
+ bool isWriteOperation() const {
+ return isa<StoreInst>(Inst) || (Flags & kCompoundRW);
+ }
+
+ Instruction *Inst;
+ unsigned Flags = 0;
+};
+
+/// Eliminates redundant TSan instrumentation using dominance analysis.
+///
+/// An instrumented access B is redundant if there exists a prior access A such
+/// that all four conditions hold:
+///
+/// (1) A dominates B — A executes on every path that reaches B, so any
+/// execution that sees a race at B also sees A beforehand.
+/// (2) A and B MustAlias — they access the same concrete memory location,
+/// so a race partner of B would also race with A.
+/// (3) Coverage: if B is a write, A must also be a write. A write races with
+/// both reads and writes, so a dominating write covers either type. A
+/// read races only with writes, so a dominating read cannot cover a
+/// subsequent write (the write could introduce a new race with a third
+/// thread's read).
+/// (4) The path from A to B is synchronization-free — no atomic, fence, or
+/// call without `nosync` appears on any CFG path between them. If such an
+/// instruction existed, a second thread could synchronize with the
+/// current thread between A and B (establishing a new happens-before
+/// edge), making it possible for B to race with that thread while A does
+/// not.
+///
+/// When all four conditions hold, any race detectable at B is already
+/// detectable at A; removing B's instrumentation is sound.
+class DominanceBasedElimination {
+public:
+ DominanceBasedElimination(SmallVectorImpl<InstructionInfo> &AllInstr,
+ DominatorTree &DT, AAResults &AA)
+ : AllInstr(AllInstr), DT(DT), AA(AA) {}
+
+ void run() { eliminate(); }
+
+private:
+ /// Returns true if the instruction cannot cause inter-thread synchronization.
+ static bool isInstrSafe(const Instruction *Inst);
+
+ /// Returns true if no synchronization exists on any CFG path from DomInst
+ /// to CurrInst. CanReachEnd must be the reverse-reachability set of
+ /// CurrInst's block, pre-computed once per CurrInst by the caller.
+ static bool
+ isPathClear(Instruction *DomInst, Instruction *CurrInst,
+ const SmallPtrSetImpl<const BasicBlock *> &CanReachEnd);
+
+ DenseMap<Instruction *, size_t> createInstrToIndexMap() const;
+
+ /// Searches AllInstr for a dominating instruction that covers AllInstr[i].
+ /// Marks AllInstr[i] for removal and returns true if one is found.
+ bool findAndMarkDominatingInstr(
+ size_t i, const DenseMap<Instruction *, size_t> &InstrToIndexMap,
+ const SmallPtrSetImpl<const BasicBlock *> &CanReachEnd,
+ SmallVectorImpl<bool> &ToRemove);
+
+ void eliminate();
+
+ SmallVectorImpl<InstructionInfo> &AllInstr;
+ DominatorTree &DT;
+ AAResults &AA;
+};
/// ThreadSanitizer: instrument the code in module to find races.
///
@@ -109,7 +202,8 @@ namespace {
/// ensures the __tsan_init function is in the list of global constructors for
/// the module.
struct ThreadSanitizer {
- ThreadSanitizer() {
+ ThreadSanitizer(DominatorTree *DT = nullptr, AAResults *AA = nullptr)
+ : DT(DT), AA(AA) {
// Check options and warn user.
if (ClInstrumentReadBeforeWrite && ClCompoundReadBeforeWrite) {
errs()
@@ -121,19 +215,6 @@ struct ThreadSanitizer {
bool sanitizeFunction(Function &F, const TargetLibraryInfo &TLI);
private:
- // Internal Instruction wrapper that contains more information about the
- // Instruction from prior analysis.
- struct InstructionInfo {
- // Instrumentation emitted for this instruction is for a compounded set of
- // read and write operations in the same basic block.
- static constexpr unsigned kCompoundRW = (1U << 0);
-
- explicit InstructionInfo(Instruction *Inst) : Inst(Inst) {}
-
- Instruction *Inst;
- unsigned Flags = 0;
- };
-
void initialize(Module &M, const TargetLibraryInfo &TLI);
bool instrumentLoadOrStore(const InstructionInfo &II, const DataLayout &DL);
bool instrumentAtomic(Instruction *I, const DataLayout &DL);
@@ -145,6 +226,9 @@ struct ThreadSanitizer {
int getMemoryAccessFuncIndex(Type *OrigTy, Value *Addr, const DataLayout &DL);
void InsertRuntimeIgnores(Function &F);
+ DominatorTree *DT = nullptr;
+ AAResults *AA = nullptr;
+
Type *IntptrTy;
FunctionCallee TsanFuncEntry;
FunctionCallee TsanFuncExit;
@@ -174,6 +258,258 @@ struct ThreadSanitizer {
FunctionCallee MemmoveFn, MemcpyFn, MemsetFn;
};
+//-----------------------------------------------------------------------------
+// DominanceBasedElimination Implementation
+//-----------------------------------------------------------------------------
+
+static bool isTsanAtomic(const Instruction *I) {
+ // TODO: Ask TTI whether synchronization scope is between threads.
+ auto SSID = getAtomicSyncScopeID(I);
+ if (!SSID)
+ return false;
+ if (isa<LoadInst>(I) || isa<StoreInst>(I))
+ return *SSID != SyncScope::SingleThread;
+ return true;
+}
+
+bool DominanceBasedElimination::isInstrSafe(const Instruction *Inst) {
+ // Atomics with inter-thread scope establish happens-before between threads
+ // (e.g., a release store followed by an acquire load synchronizes the two
+ // threads). Any such operation on the path from A to B would allow another
+ // thread to "see" the state after A but before B, potentially racing with B
+ // while A does not observe the race.
+ if (isTsanAtomic(Inst))
+ return false;
+
+ // A function call may contain mutex unlocks, thread spawns, or other
+ // release-like operations that create new happens-before edges. A call is
+ // safe only if it is guaranteed not to synchronize with other threads in
+ // any way. hasFnAttr checks both the call-site attribute and the callee's
+ // function attributes, so it handles direct calls, indirect calls with a
+ // nosync call-site annotation, and intrinsics uniformly.
+ if (const auto *CB = dyn_cast<CallBase>(Inst))
+ return CB->hasFnAttr(Attribute::NoSync);
+
+ // Fences are handled by isTsanAtomic above. All remaining instructions
+ // (arithmetic, branches, GEPs, etc.) cannot establish inter-thread
+ // happens-before and are safe to cross.
+ return true;
+}
+
+DenseMap<Instruction *, size_t>
+DominanceBasedElimination::createInstrToIndexMap() const {
+ DenseMap<Instruction *, size_t> InstrToIndexMap;
+ InstrToIndexMap.reserve(AllInstr.size());
+ for (size_t i = 0; i < AllInstr.size(); ++i)
+ InstrToIndexMap[AllInstr[i].Inst] = i;
+ return InstrToIndexMap;
+}
+
+bool DominanceBasedElimination::isPathClear(
+ Instruction *DomInst, Instruction *CurrInst,
+ const SmallPtrSetImpl<const BasicBlock *> &CanReachEnd) {
+ BasicBlock *DomBB = DomInst->getParent();
+ BasicBlock *CurrBB = CurrInst->getParent();
+
+ // Intra-BB: only the instructions strictly between DomInst and CurrInst
+ // can introduce synchronization. Scan the open interval (DomInst, CurrInst).
+ if (DomBB == CurrBB) {
+ for (auto It = std::next(DomInst->getIterator());
+ It != CurrInst->getIterator(); ++It)
+ if (!isInstrSafe(&*It))
+ return false;
+ return true;
+ }
+
+ // Inter-BB: three regions must all be synchronization-free.
+ //
+ // Region 1 — suffix of DomBB (instructions after DomInst in DomInst's block).
+ // These execute on every path from DomInst before leaving the block.
+ for (auto It = std::next(DomInst->getIterator()); It != DomBB->end(); ++It)
+ if (!isInstrSafe(&*It))
+ return false;
+
+ // Region 2 — prefix of CurrBB (instructions before CurrInst in CurrInst's
+ // block). These execute on every path that arrives at CurrInst.
+ for (auto It = CurrBB->begin(); It != CurrInst->getIterator(); ++It)
+ if (!isInstrSafe(&*It))
+ return false;
+
+ // Region 3 — all intermediate blocks that lie on at least one path from
+ // DomBB to CurrBB. We restrict traversal to blocks in CanReachEnd (the
+ // reverse-reachability set of CurrBB computed by the caller) so that we
+ // only inspect blocks that actually lie between the two endpoints. Blocks
+ // that cannot reach CurrBB are on diverging paths that never connect the
+ // two accesses and must not veto the elimination.
+ SmallPtrSet<const BasicBlock *, 32> Visited;
+ SmallVector<const BasicBlock *, 16> Worklist;
+ for (const BasicBlock *Succ : successors(DomBB)) {
+ if (CanReachEnd.count(Succ) && Succ != CurrBB &&
+ Visited.insert(Succ).second)
+ Worklist.push_back(Succ);
+ }
+
+ while (!Worklist.empty()) {
+ const BasicBlock *BB = Worklist.pop_back_val();
+ for (const Instruction &I : *BB)
+ if (!isInstrSafe(&I))
+ return false;
+ for (const BasicBlock *Succ : successors(BB))
+ if (CanReachEnd.count(Succ) && Succ != CurrBB &&
+ Visited.insert(Succ).second)
+ Worklist.push_back(Succ);
+ }
+ return true;
+}
+
+bool DominanceBasedElimination::findAndMarkDominatingInstr(
+ size_t i, const DenseMap<Instruction *, size_t> &InstrToIndexMap,
+ const SmallPtrSetImpl<const BasicBlock *> &CanReachEnd,
+ SmallVectorImpl<bool> &ToRemove) {
+ LLVM_DEBUG(dbgs() << "\nAnalyzing: " << *(AllInstr[i].Inst) << "\n");
+ const InstructionInfo &CurrII = AllInstr[i];
+ Instruction *CurrInst = CurrII.Inst;
+ const BasicBlock *CurrBB = CurrInst->getParent();
+
+ const DomTreeNode *CurrDTNode = DT.getNode(CurrBB);
+ if (!CurrDTNode)
+ return false;
+
+ // A dominating access A must be in a block that is an ancestor of CurrBB in
+ // the dominator tree (condition 1). Walking up from CurrBB's node to the
+ // tree root visits exactly those blocks, so we only scan instrumented
+ // accesses inside them. This is more efficient than scanning all of AllInstr
+ // and filtering by domination after the fact.
+ for (const DomTreeNode *IDomNode = CurrDTNode; IDomNode;
+ IDomNode = IDomNode->getIDom()) {
+ const BasicBlock *DomBB = IDomNode->getBlock();
+ if (!DomBB)
+ break;
+
+ // When DomBB == CurrBB, the dominating instruction must appear before
+ // CurrInst in program order; instructions after it do not dominate it.
+ auto EndIt = (DomBB == CurrBB) ? CurrInst->getIterator() : DomBB->end();
+
+ for (auto InstIt = DomBB->begin(); InstIt != EndIt; ++InstIt) {
+ LLVM_DEBUG(dbgs() << "Candidate: " << *InstIt << "\n");
+
+ const auto It = InstrToIndexMap.find(&*InstIt);
+ if (It == InstrToIndexMap.end() || ToRemove[It->second])
+ continue; // not an instrumented access, or already eliminated
+
+ const size_t DomIndex = It->second;
+ const InstructionInfo &DomII = AllInstr[DomIndex];
+ Instruction *DomInst = DomII.Inst;
+
+ auto IsVolatile = [](const Instruction *I) {
+ if (const auto *L = dyn_cast<LoadInst>(I))
+ return L->isVolatile();
+ if (const auto *S = dyn_cast<StoreInst>(I))
+ return S->isVolatile();
+ return false;
+ };
+ // With -tsan-distinguish-volatile, volatile and non-volatile accesses
+ // emit different runtime calls and must not be merged.
+ if (ClDistinguishVolatile &&
+ (IsVolatile(DomInst) || IsVolatile(CurrInst)))
+ continue;
+
+ // Condition (2): same memory location.
+ // isMustAlias checks that the base pointers are identical but does NOT
+ // compare access sizes — two MemoryLocations with the same pointer but
+ // different sizes both return MustAlias. We therefore check sizes
+ // separately: DomInst must cover at least as many bytes as CurrInst,
+ // otherwise races on the extra bytes in CurrInst's range would not be
+ // detected by DomInst's instrumentation call.
+ const MemoryLocation CurrLoc = MemoryLocation::get(CurrInst);
+ const MemoryLocation DomLoc = MemoryLocation::get(DomInst);
+ if (!AA.isMustAlias(CurrLoc, DomLoc))
+ continue;
+ // Require both sizes to be known, non-scalable, and DomSize >= CurrSize.
+ if (!DomLoc.Size.hasValue() || !CurrLoc.Size.hasValue() ||
+ DomLoc.Size.isScalable() || CurrLoc.Size.isScalable() ||
+ DomLoc.Size.getValue().getFixedValue() <
+ CurrLoc.Size.getValue().getFixedValue())
+ continue;
+
+ // Condition (3): write coverage rule.
+ // A write races with both reads and writes on other threads, so a
+ // dominating write makes CurrInst's check redundant regardless of
+ // whether CurrInst is a read or a write.
+ // A read races only with writes, so a dominating read cannot subsume
+ // a subsequent write: the write might race with a third thread's read
+ // that the dominating read would not catch.
+ if (!DomII.isWriteOperation() && CurrII.isWriteOperation())
+ continue;
+
+ // Condition (4): synchronization-free path (checked last because it
+ // involves CFG traversal and is the most expensive test).
+ if (isPathClear(DomInst, CurrInst, CanReachEnd)) {
+ LLVM_DEBUG(dbgs() << "TSAN: Omitting " << *CurrInst << " (dominated by "
+ << *DomInst << ")\n");
+ ToRemove[i] = true;
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+void DominanceBasedElimination::eliminate() {
+ LLVM_DEBUG(dbgs() << "Starting dominance-based elimination\n");
+ if (AllInstr.empty())
+ return;
+
+ SmallVector<bool, 16> ToRemove(AllInstr.size(), false);
+ unsigned RemovedCount = 0;
+ const DenseMap<Instruction *, size_t> InstrToIndexMap =
+ createInstrToIndexMap();
+
+ for (size_t i = 0; i < AllInstr.size(); ++i) {
+ if (ToRemove[i])
+ continue;
+
+ // Build the reverse-reachability set of CurrBB: the set of all blocks
+ // from which CurrBB is reachable. isPathClear uses this to restrict its
+ // forward BFS to blocks that actually lie on a path from DomBB to CurrBB.
+ //
+ // We compute it here — once per CurrInst — rather than inside isPathClear,
+ // because findAndMarkDominatingInstr may call isPathClear several times
+ // for different DomInst candidates that all share the same CurrBB.
+ // Pre-computing avoids repeating the reverse BFS for each candidate.
+ const BasicBlock *CurrBB = AllInstr[i].Inst->getParent();
+ SmallPtrSet<const BasicBlock *, 32> CanReachEnd;
+ SmallVector<const BasicBlock *, 16> RBFSWorklist;
+ CanReachEnd.insert(CurrBB);
+ RBFSWorklist.push_back(CurrBB);
+ while (!RBFSWorklist.empty()) {
+ const BasicBlock *BB = RBFSWorklist.pop_back_val();
+ for (const BasicBlock *Pred : predecessors(BB))
+ if (CanReachEnd.insert(Pred).second)
+ RBFSWorklist.push_back(Pred);
+ }
+
+ if (findAndMarkDominatingInstr(i, InstrToIndexMap, CanReachEnd, ToRemove))
+ RemovedCount++;
+ }
+
+ LLVM_DEBUG(dbgs() << "\nFinal instruction status:\n";
+ for (size_t i = 0; i < AllInstr.size(); ++i) dbgs()
+ << "[" << (ToRemove[i] ? "REMOVED" : "KEPT") << "]\t"
+ << *AllInstr[i].Inst << "\n");
+
+ if (RemovedCount > 0) {
+ auto It = ToRemove.begin();
+ erase_if(AllInstr, [&](const InstructionInfo &) { return *It++; });
+ NumOmittedByDominance += RemovedCount;
+ }
+ LLVM_DEBUG(dbgs() << "Dominance elimination complete\n");
+}
+
+//-----------------------------------------------------------------------------
+// ThreadSanitizer Implementation
+//-----------------------------------------------------------------------------
+
void insertModuleCtor(Module &M) {
getOrCreateSanitizerCtorAndInitFunctions(
M, kTsanModuleCtorName, kTsanInitName, /*InitArgTypes=*/{},
@@ -186,7 +522,15 @@ void insertModuleCtor(Module &M) {
PreservedAnalyses ThreadSanitizerPass::run(Function &F,
FunctionAnalysisManager &FAM) {
- ThreadSanitizer TSan;
+ DominatorTree *DT = nullptr;
+ AAResults *AA = nullptr;
+
+ if (ClUseDominanceAnalysis) {
+ DT = &FAM.getResult<DominatorTreeAnalysis>(F);
+ AA = &FAM.getResult<AAManager>(F);
+ }
+
+ ThreadSanitizer TSan(DT, AA);
if (TSan.sanitizeFunction(F, FAM.getResult<TargetLibraryAnalysis>(F)))
return PreservedAnalyses::none();
return PreservedAnalyses::all();
@@ -474,16 +818,6 @@ void ThreadSanitizer::chooseInstructionsToInstrument(
Local.clear();
}
-static bool isTsanAtomic(const Instruction *I) {
- // TODO: Ask TTI whether synchronization scope is between threads.
- auto SSID = getAtomicSyncScopeID(I);
- if (!SSID)
- return false;
- if (isa<LoadInst>(I) || isa<StoreInst>(I))
- return *SSID != SyncScope::SingleThread;
- return true;
-}
-
void ThreadSanitizer::InsertRuntimeIgnores(Function &F) {
InstrumentationIRBuilder IRB(&F.getEntryBlock(),
F.getEntryBlock().getFirstNonPHIIt());
@@ -545,6 +879,11 @@ bool ThreadSanitizer::sanitizeFunction(Function &F,
chooseInstructionsToInstrument(LocalLoadsAndStores, AllLoadsAndStores, DL);
}
+ if (ClUseDominanceAnalysis && DT && AA) {
+ DominanceBasedElimination DBE(AllLoadsAndStores, *DT, *AA);
+ DBE.run();
+ }
+
// We have collected all loads and stores.
// FIXME: many of these accesses do not need to be checked for races
// (e.g. variables that do not escape, etc).
diff --git a/llvm/test/Instrumentation/ThreadSanitizer/capture-no-omit.ll b/llvm/test/Instrumentation/ThreadSanitizer/capture-no-omit.ll
index cae04936002cd..f7d6eb56ba94f 100644
--- a/llvm/test/Instrumentation/ThreadSanitizer/capture-no-omit.ll
+++ b/llvm/test/Instrumentation/ThreadSanitizer/capture-no-omit.ll
@@ -1,4 +1,4 @@
-; RUN: opt < %s -passes=tsan -tsan-omit-by-pointer-capturing=0 -S | FileCheck %s
+; RUN: opt < %s -passes=tsan -tsan-omit-by-pointer-capturing=0 -tsan-use-dominance-analysis=0 -S | FileCheck %s
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64-S128"
diff --git a/llvm/test/Instrumentation/ThreadSanitizer/dominance-elimination.ll b/llvm/test/Instrumentation/ThreadSanitizer/dominance-elimination.ll
new file mode 100644
index 0000000000000..38a38907c6620
--- /dev/null
+++ b/llvm/test/Instrumentation/ThreadSanitizer/dominance-elimination.ll
@@ -0,0 +1,789 @@
+; RUN: opt < %s -passes=tsan -S | FileCheck %s
+; RUN: opt < %s -passes=tsan -tsan-use-dominance-analysis=false -S | FileCheck %s --check-prefix=NODOM
+; RUN: opt < %s -passes=tsan -tsan-distinguish-volatile -S | FileCheck %s --check-prefix=VOLATILE
+
+; Tests for TSan dominance-based redundant instrumentation elimination.
+; Redundant instrumentation is removed when one access dominates another to
+; the same location with no synchronization on any path between them.
+;
+; Check prefixes:
+; CHECK - default run (optimization enabled)
+; NODOM - optimization disabled; all accesses must remain instrumented
+; VOLATILE - -tsan-distinguish-volatile enabled; volatile and non-volatile
+; accesses emit different runtime calls and must not be merged
+
+target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64-S128"
+
+ at g1 = global i32 0, align 4
+ at g2 = global i32 0, align 4
+ at arr = global [5 x i32] zeroinitializer, align 4
+
+; Unsafe call (no nosync): blocks dominance-based elimination.
+declare void @ext_call()
+; nosync but no willreturn/nounwind: safe for dom-elimination, blocks postdom.
+declare void @nosync_func() #0
+; nosync + willreturn + nounwind: safe for both dom- and postdom-elimination.
+declare void @willreturn_func() #1
+; Unsafe function returning i32, used in loop tests.
+declare i32 @ext_check(...)
+
+; ===========================================================================
+; Intra-block dominance
+; ===========================================================================
+
+; First write dominates second write to the same location.
+define void @intra_block_write_write() nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ store i32 2, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @intra_block_write_write
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK-NOT: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+;
+; NODOM-LABEL: define void @intra_block_write_write
+; NODOM: call void @__tsan_write4(ptr @g1)
+; NODOM: call void @__tsan_write4(ptr @g1)
+; NODOM: ret void
+
+; Write dominates following read: write covers read, so read is removed.
+define void @intra_block_write_read() nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ %val = load i32, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @intra_block_write_read
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK-NOT: call void @__tsan_read4(ptr @g1)
+; CHECK: ret void
+
+; First read dominates second read to the same location.
+define void @intra_block_read_read() nounwind uwtable sanitize_thread {
+entry:
+ %v1 = load i32, ptr @g1, align 4
+ %v2 = load i32, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @intra_block_read_read
+; CHECK: call void @__tsan_read4(ptr @g1)
+; CHECK-NOT: call void @__tsan_read4(ptr @g1)
+; CHECK: ret void
+
+; A dominating read does NOT eliminate a write on a dominated path.
+; The read-before-write elimination in chooseInstructionsToInstrument only
+; applies within the same basic block, so this uses an inter-block scenario.
+; The write is only on one branch so it is NOT the post-dominator of the
+; read; the dominance check therefore applies in isolation.
+define void @dom_read_does_not_cover_write(i1 %cond) nounwind uwtable sanitize_thread {
+entry:
+ %v = load i32, ptr @g1, align 4
+ br i1 %cond, label %write.path, label %skip
+write.path:
+ store i32 1, ptr @g1, align 4
+ br label %end
+skip:
+ br label %end
+end:
+ ret void
+}
+; CHECK-LABEL: define void @dom_read_does_not_cover_write
+; The read dominates the write, but a read cannot cover write-write races.
+; Both must remain instrumented.
+; CHECK: call void @__tsan_read4(ptr @g1)
+; CHECK: write.path:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+
+; ===========================================================================
+; Path safety
+; ===========================================================================
+
+; An inter-thread atomic on the path is a synchronization point: no elimination.
+; (isTsanAtomic returns true for cross-thread scope.)
+define void @path_dirty_atomic_interthread(ptr %p, ptr %flag) nounwind sanitize_thread {
+entry:
+ store i32 1, ptr %p, align 4
+ %v = load atomic i32, ptr %flag acquire, align 4
+ store i32 2, ptr %p, align 4
+ ret void
+}
+; CHECK-LABEL: define void @path_dirty_atomic_interthread
+; CHECK: call void @__tsan_write4(ptr %p)
+; CHECK: call void @__tsan_write4(ptr %p)
+; CHECK: ret void
+
+; A singlethread-scoped atomic on the path does NOT block elimination.
+; isTsanAtomic returns false for singlethread scope on load/store, so
+; it is treated as a plain access with no inter-thread synchronization.
+define void @path_clear_atomic_singlethread(ptr %p, ptr %q) nounwind sanitize_thread {
+entry:
+ store i32 1, ptr %p, align 4
+ store atomic i32 0, ptr %q syncscope("singlethread") seq_cst, align 4
+ store i32 2, ptr %p, align 4
+ ret void
+}
+; CHECK-LABEL: define void @path_clear_atomic_singlethread
+; CHECK: call void @__tsan_write4(ptr %p)
+; CHECK-NOT: call void @__tsan_write4(ptr %p)
+; CHECK: ret void
+
+; A singlethread fence IS treated as unsafe (conservative: isTsanAtomic
+; returns true for any non-load/store instruction with a sync scope,
+; regardless of whether the scope is singlethread or system). Both writes
+; remain instrumented.
+define void @path_dirty_fence_singlethread(ptr %p) nounwind sanitize_thread {
+entry:
+ store i32 1, ptr %p, align 4
+ fence syncscope("singlethread") seq_cst
+ store i32 2, ptr %p, align 4
+ ret void
+}
+; CHECK-LABEL: define void @path_dirty_fence_singlethread
+; CHECK: call void @__tsan_write4(ptr %p)
+; CHECK: call void @__tsan_write4(ptr %p)
+; CHECK: ret void
+
+; An unsafe call between two accesses makes the path dirty: no elimination.
+define void @path_dirty_call() nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ call void @ext_call()
+ store i32 2, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @path_dirty_call
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+
+; A nosync call does not block dominance elimination.
+define void @path_clear_nosync_call() nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ call void @nosync_func()
+ store i32 2, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @path_clear_nosync_call
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK-NOT: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+
+; An indirect call with nosync at the call site does not block elimination.
+; #0 = { nosync } — the attribute group is defined at the bottom of this file.
+; This exercises the call-site attribute path in isInstrSafe (hasFnAttr).
+define void @path_clear_indirect_nosync_callsite(ptr %fn) nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ call void %fn() #0
+ store i32 2, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @path_clear_indirect_nosync_callsite
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK-NOT: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+
+; ===========================================================================
+; Inter-block dominance
+; ===========================================================================
+
+; A write in the entry block dominates writes in both branches of a diamond.
+define void @inter_block_dom(i1 %cond) nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ br i1 %cond, label %if.then, label %if.else
+if.then:
+ store i32 2, ptr @g1, align 4
+ br label %if.end
+if.else:
+ store i32 3, ptr @g1, align 4
+ br label %if.end
+if.end:
+ ret void
+}
+; CHECK-LABEL: define void @inter_block_dom
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: if.then:
+; CHECK-NOT: call void @__tsan_write4(ptr @g1)
+; CHECK: if.else:
+; CHECK-NOT: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+;
+; NODOM-LABEL: define void @inter_block_dom
+; NODOM: call void @__tsan_write4(ptr @g1)
+; NODOM: call void @__tsan_write4(ptr @g1)
+; NODOM: call void @__tsan_write4(ptr @g1)
+; NODOM: ret void
+
+; ===========================================================================
+; Post-dominance
+;
+; Post-dominance elimination is not yet implemented (planned for a follow-up
+; patch). All accesses in this section remain fully instrumented.
+; ===========================================================================
+
+; Write at the merge point post-dominates writes in both branches.
+define void @postdom_diamond(i1 %cond) nounwind uwtable sanitize_thread {
+entry:
+ br i1 %cond, label %if.then, label %if.else
+if.then:
+ store i32 2, ptr @g1, align 4
+ br label %if.end
+if.else:
+ store i32 3, ptr @g1, align 4
+ br label %if.end
+if.end:
+ store i32 4, ptr @g1, align 4
+ ret void
+}
+; No dominator eliminates the branch writes: the entry block has no write to
+; @g1, so neither branch write is dominated by a prior write. All three writes
+; remain (post-dom elimination not yet implemented).
+; CHECK-LABEL: define void @postdom_diamond
+; CHECK: if.then:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: if.else:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: if.end:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+;
+; NODOM-LABEL: define void @postdom_diamond
+; NODOM: call void @__tsan_write4(ptr @g1)
+; NODOM: call void @__tsan_write4(ptr @g1)
+; NODOM: call void @__tsan_write4(ptr @g1)
+; NODOM: ret void
+
+; Write at the merge point post-dominates reads in both branches.
+; Since a write covers both read and write, the reads are eliminated.
+define void @postdom_read_covered_by_write(i1 %cond) nounwind uwtable sanitize_thread {
+entry:
+ br i1 %cond, label %if.then, label %if.else
+if.then:
+ %v1 = load i32, ptr @g1, align 4
+ br label %if.end
+if.else:
+ %v2 = load i32, ptr @g1, align 4
+ br label %if.end
+if.end:
+ store i32 0, ptr @g1, align 4
+ ret void
+}
+; The write in if.end does not dominate the reads in the branches (the entry
+; block has no write). All three accesses remain (post-dom not yet implemented).
+; CHECK-LABEL: define void @postdom_read_covered_by_write
+; CHECK: if.then:
+; CHECK: call void @__tsan_read4(ptr @g1)
+; CHECK: if.else:
+; CHECK: call void @__tsan_read4(ptr @g1)
+; CHECK: if.end:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+
+; Read at the merge point post-dominates reads in both branches.
+define void @postdom_read_read(i1 %cond) nounwind uwtable sanitize_thread {
+entry:
+ br i1 %cond, label %if.then, label %if.else
+if.then:
+ %v1 = load i32, ptr @g1, align 4
+ br label %if.end
+if.else:
+ %v2 = load i32, ptr @g1, align 4
+ br label %if.end
+if.end:
+ %v3 = load i32, ptr @g1, align 4
+ ret void
+}
+; The read in if.end does not dominate the reads in the branches. All three
+; reads remain (post-dom not yet implemented).
+; CHECK-LABEL: define void @postdom_read_read
+; CHECK: if.then:
+; CHECK: call void @__tsan_read4(ptr @g1)
+; CHECK: if.else:
+; CHECK: call void @__tsan_read4(ptr @g1)
+; CHECK: if.end:
+; CHECK: call void @__tsan_read4(ptr @g1)
+; CHECK: ret void
+
+; A read at the merge point post-dominates writes in both branches, but a
+; post-dominating read does NOT cover write-write race detection. Both writes
+; must remain instrumented.
+define void @postdom_read_does_not_cover_write(i1 %cond) nounwind uwtable sanitize_thread {
+entry:
+ br i1 %cond, label %if.then, label %if.else
+if.then:
+ store i32 1, ptr @g1, align 4
+ br label %if.end
+if.else:
+ store i32 2, ptr @g1, align 4
+ br label %if.end
+if.end:
+ %v = load i32, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @postdom_read_does_not_cover_write
+; CHECK: if.then:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: if.else:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: if.end:
+; CHECK: call void @__tsan_read4(ptr @g1)
+; CHECK: ret void
+
+; ===========================================================================
+; Multi-path: dirty vs clean
+; ===========================================================================
+
+; One branch carries an unsafe call: the read at the merge is not eliminated.
+define void @multi_path_dirty(i1 %cond) nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ br i1 %cond, label %then, label %else
+then:
+ call void @ext_call()
+ br label %merge
+else:
+ call void @nosync_func()
+ br label %merge
+merge:
+ %v = load i32, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @multi_path_dirty
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: merge:
+; CHECK: call void @__tsan_read4(ptr @g1)
+; CHECK: ret void
+
+; Both branches carry only nosync calls: the read at the merge is eliminated.
+define void @multi_path_clean(i1 %cond) nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ br i1 %cond, label %then, label %else
+then:
+ call void @nosync_func()
+ br label %merge
+else:
+ call void @nosync_func()
+ br label %merge
+merge:
+ %v = load i32, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @multi_path_clean
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: merge:
+; CHECK-NOT: call void @__tsan_read4(ptr @g1)
+; CHECK: ret void
+
+; Adjacent stores in the same block are eliminated; a dirty path to the merge
+; block keeps the final store instrumented.
+define void @mixed_intra_inter(i1 %cond) nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ store i32 2, ptr @g1, align 4
+ br i1 %cond, label %dirty, label %clean
+dirty:
+ call void @ext_call()
+ br label %merge
+clean:
+ call void @nosync_func()
+ br label %merge
+merge:
+ store i32 3, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @mixed_intra_inter
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK-NOT: call void @__tsan_write4(ptr @g1)
+; CHECK: merge:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+
+; ===========================================================================
+; Path safety: dirty suffix / prefix / unrelated path
+; ===========================================================================
+
+; Unsafe call in suffix of start block blocks postdom elimination.
+define void @postdom_dirty_start_suffix(i1 %cond) nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ call void @ext_call()
+ br i1 %cond, label %path.then, label %path.else
+path.then:
+ br label %merge
+path.else:
+ br label %merge
+merge:
+ %v = load i32, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @postdom_dirty_start_suffix
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: merge:
+; CHECK: call void @__tsan_read4(ptr @g1)
+; CHECK: ret void
+
+; Unsafe call in prefix of end block blocks dom elimination.
+define void @postdom_dirty_end_prefix() nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ br label %end
+end:
+ call void @ext_call()
+ %v = load i32, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @postdom_dirty_end_prefix
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: call void @__tsan_read4(ptr @g1)
+; CHECK: ret void
+
+; A dirty path that does not reach the end block must not block elimination.
+define void @postdom_dirty_unrelated_path(i1 %cond) nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ br i1 %cond, label %to.end, label %to.dead
+to.end:
+ br label %end
+to.dead:
+ call void @ext_call()
+ br label %dead
+dead:
+ ret void
+end:
+ %v = load i32, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @postdom_dirty_unrelated_path
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: end:
+; CHECK-NOT: call void @__tsan_read4(ptr @g1)
+; CHECK: ret void
+
+; ===========================================================================
+; Loops
+; ===========================================================================
+
+; Loop with an unsafe call in the condition: dom-safety fails, both stores
+; remain instrumented.
+define void @postdom_loop() nounwind uwtable sanitize_thread {
+entry:
+ br label %while.cond
+while.cond:
+ %v = call i32 (...) @ext_check()
+ %tobool = icmp ne i32 %v, 0
+ br i1 %tobool, label %while.body, label %while.end
+while.body:
+ store i32 1, ptr @g1, align 4
+ br label %while.cond
+while.end:
+ store i32 2, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @postdom_loop
+; CHECK: while.body:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: while.end:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+
+; Pure loop (only branch instructions, no calls or atomics): the store inside
+; the loop body is not dominated by the store after the loop (while.body does
+; not dominate while.end), and LLVM's PDT does not establish post-dominance
+; across a potentially-infinite loop. Both stores must remain instrumented.
+define void @postdom_pure_loop(i1 %cond) nounwind uwtable sanitize_thread {
+entry:
+ br label %while.cond
+while.cond:
+ br i1 %cond, label %while.body, label %while.end
+while.body:
+ store i32 1, ptr @g1, align 4
+ br label %while.cond
+while.end:
+ store i32 2, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @postdom_pure_loop
+; CHECK: while.body:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: while.end:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+
+; ===========================================================================
+; Post-dominance: call-attribute safety
+; ===========================================================================
+
+; A nosync call without willreturn is safe for dom-elimination, but blocks
+; postdom-elimination (may not return, so the postdominator might never execute).
+define void @postdom_blocked_nosync_call(i1 %cond) nounwind uwtable sanitize_thread {
+entry:
+ br i1 %cond, label %if.then, label %if.else
+if.then:
+ store i32 1, ptr @g1, align 4
+ call void @nosync_func()
+ br label %if.end
+if.else:
+ br label %if.end
+if.end:
+ store i32 2, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @postdom_blocked_nosync_call
+; CHECK: if.then:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: if.end:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+
+; A nosync call (safe for dom, unsafe for postdom) in a separate INTERMEDIATE
+; block between CurrInst's block and DomInst's block must block postdom
+; elimination via the cone traversal (not the local suffix/prefix checks).
+; This directly exercises the intermediate-block safety check in
+; traverseReachableAndCheckSafety.
+define void @postdom_blocked_nosync_call_intermediate(i1 %cond) nounwind uwtable sanitize_thread {
+entry:
+ br i1 %cond, label %if.then, label %middle
+if.then:
+ store i32 1, ptr @g1, align 4
+ br label %middle
+middle:
+ call void @nosync_func()
+ br label %exit
+exit:
+ store i32 2, ptr @g1, align 4
+ ret void
+}
+; The nosync call in %middle is not in the suffix of %if.then or in the prefix
+; of %exit, so only the cone traversal can detect it. Both stores must remain.
+; CHECK-LABEL: define void @postdom_blocked_nosync_call_intermediate
+; CHECK: if.then:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: exit:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+
+; A nosync + willreturn + nounwind call is safe for postdom-elimination.
+define void @postdom_allowed_willreturn_call(i1 %cond) nounwind uwtable sanitize_thread {
+entry:
+ br i1 %cond, label %if.then, label %if.else
+if.then:
+ store i32 1, ptr @g1, align 4
+ call void @willreturn_func()
+ br label %if.end
+if.else:
+ br label %if.end
+if.end:
+ store i32 2, ptr @g1, align 4
+ ret void
+}
+; if.then does not dominate if.end (there is a path entry->if.else->if.end).
+; Both writes remain (post-dom not yet implemented).
+; CHECK-LABEL: define void @postdom_allowed_willreturn_call
+; CHECK: if.then:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: if.end:
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+
+; ===========================================================================
+; Synchronization barriers
+; ===========================================================================
+
+; An atomic RMW operation is a synchronization point: no elimination.
+define void @atomic_blocks_dom_elim() nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ %old = atomicrmw add ptr @g1, i32 1 seq_cst
+ store i32 2, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @atomic_blocks_dom_elim
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+
+; A seq_cst fence is a synchronization point: no elimination.
+define void @fence_blocks_dom_elim() nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ fence seq_cst
+ store i32 2, ptr @g1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @fence_blocks_dom_elim
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: ret void
+
+; ===========================================================================
+; Access size compatibility
+; ===========================================================================
+
+; A dominating write of smaller size must NOT eliminate a larger write: the
+; 4-byte instrumentation does not cover bytes 4-7 of the 8-byte access.
+define void @size_mismatch_small_dom(ptr %p) nounwind sanitize_thread {
+entry:
+ store i32 1, ptr %p, align 4
+ store i64 2, ptr %p, align 8
+ ret void
+}
+; CHECK-LABEL: define void @size_mismatch_small_dom
+; CHECK: call void @__tsan_write4(ptr %p)
+; CHECK: call void @__tsan_write8(ptr %p)
+; CHECK: ret void
+
+; A dominating write of larger size CAN eliminate a smaller write: write8
+; covers the full [p, p+4) range that write4 would instrument.
+define void @size_mismatch_large_dom(ptr %p) nounwind sanitize_thread {
+entry:
+ store i64 1, ptr %p, align 8
+ store i32 2, ptr %p, align 4
+ ret void
+}
+; CHECK-LABEL: define void @size_mismatch_large_dom
+; CHECK: call void @__tsan_write8(ptr %p)
+; CHECK-NOT: call void @__tsan_write4(ptr %p)
+; CHECK: ret void
+
+; ===========================================================================
+; Alias analysis
+; ===========================================================================
+
+; Accesses to distinct globals are NoAlias: no elimination.
+define void @no_alias() nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ store i32 2, ptr @g2, align 4
+ ret void
+}
+; CHECK-LABEL: define void @no_alias
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: call void @__tsan_write4(ptr @g2)
+; CHECK: ret void
+
+; Zero-index GEP is MustAlias with the base: elimination fires.
+define void @mustalias_gep0() nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ %p = getelementptr i32, ptr @g1, i64 0
+ %v = load i32, ptr %p, align 4
+ ret void
+}
+; CHECK-LABEL: define void @mustalias_gep0
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK-NOT: call void @__tsan_read4(
+; CHECK: ret void
+
+; Different GEP offsets into the same array are NoAlias: no elimination.
+define void @noalias_different_offsets() nounwind uwtable sanitize_thread {
+entry:
+ %p0 = getelementptr [5 x i32], ptr @arr, i64 0, i64 0
+ %p1 = getelementptr [5 x i32], ptr @arr, i64 0, i64 1
+ store i32 1, ptr %p0, align 4
+ store i32 2, ptr %p1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @noalias_different_offsets
+; CHECK: call void @__tsan_write4(
+; CHECK: call void @__tsan_write4(
+; CHECK: ret void
+
+; Identical GEP offsets into the same array are MustAlias: elimination fires.
+define void @mustalias_same_offsets() nounwind uwtable sanitize_thread {
+entry:
+ %p0 = getelementptr [5 x i32], ptr @arr, i64 0, i64 1
+ %p1 = getelementptr [5 x i32], ptr @arr, i64 0, i64 1
+ store i32 1, ptr %p0, align 4
+ store i32 2, ptr %p1, align 4
+ ret void
+}
+; CHECK-LABEL: define void @mustalias_same_offsets
+; CHECK: call void @__tsan_write4(
+; CHECK-NOT: call void @__tsan_write4(
+; CHECK: ret void
+
+; phi selecting between two globals is MayAlias: no elimination.
+define void @mayalias_phi(i1 %c) nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ br i1 %c, label %A, label %B
+A:
+ br label %join
+B:
+ br label %join
+join:
+ %p = phi ptr [ @g1, %A ], [ @g2, %B ]
+ %v = load i32, ptr %p, align 4
+ ret void
+}
+; CHECK-LABEL: define void @mayalias_phi
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: call void @__tsan_read4(
+; CHECK: ret void
+
+; ptrtoint/inttoptr round-trip breaks MustAlias: no elimination.
+define void @noalias_ptr_roundtrip() nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ %i = ptrtoint ptr @g1 to i64
+ %p2 = inttoptr i64 %i to ptr
+ %v = load i32, ptr %p2, align 4
+ ret void
+}
+; CHECK-LABEL: define void @noalias_ptr_roundtrip
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK: call void @__tsan_read4(
+; CHECK: ret void
+
+; select with identical arms is MustAlias: elimination fires.
+define void @mustalias_select_same_ptr(i1 %c) nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ %p = select i1 %c, ptr @g1, ptr @g1
+ %v = load i32, ptr %p, align 4
+ ret void
+}
+; CHECK-LABEL: define void @mustalias_select_same_ptr
+; CHECK: call void @__tsan_write4(ptr @g1)
+; CHECK-NOT: call void @__tsan_read4(
+; CHECK: ret void
+
+; ===========================================================================
+; Volatile (VOLATILE check prefix only)
+;
+; When -tsan-distinguish-volatile is active, volatile and non-volatile accesses
+; to the same address emit different TSan calls and must never be merged.
+; ===========================================================================
+
+; Non-volatile write followed by volatile write: both must be kept.
+define void @write_then_volatile_write() nounwind uwtable sanitize_thread {
+entry:
+ store i32 1, ptr @g1, align 4
+ store volatile i32 2, ptr @g1, align 4
+ ret void
+}
+; VOLATILE-LABEL: define void @write_then_volatile_write
+; VOLATILE: call void @__tsan_write4(ptr @g1)
+; VOLATILE: call void @__tsan_volatile_write4(ptr @g1)
+; VOLATILE: ret void
+
+; Volatile write followed by non-volatile write: both must be kept.
+define void @volatile_write_then_write() nounwind uwtable sanitize_thread {
+entry:
+ store volatile i32 1, ptr @g1, align 4
+ store i32 2, ptr @g1, align 4
+ ret void
+}
+; VOLATILE-LABEL: define void @volatile_write_then_write
+; VOLATILE: call void @__tsan_volatile_write4(ptr @g1)
+; VOLATILE: call void @__tsan_write4(ptr @g1)
+; VOLATILE: ret void
+
+attributes #0 = { nosync }
+attributes #1 = { nosync willreturn nounwind }
diff --git a/llvm/test/Instrumentation/ThreadSanitizer/read_before_write.ll b/llvm/test/Instrumentation/ThreadSanitizer/read_before_write.ll
index 1ef283d8f8f4c..7c2de00640785 100644
--- a/llvm/test/Instrumentation/ThreadSanitizer/read_before_write.ll
+++ b/llvm/test/Instrumentation/ThreadSanitizer/read_before_write.ll
@@ -1,7 +1,7 @@
-; RUN: opt < %s -passes=tsan -S | FileCheck --check-prefixes=CHECK,CHECK-OPT %s
-; RUN: opt < %s -passes=tsan -tsan-instrument-read-before-write -S | FileCheck %s --check-prefixes=CHECK,CHECK-UNOPT
-; RUN: opt < %s -passes=tsan -tsan-compound-read-before-write -S | FileCheck %s --check-prefixes=CHECK,CHECK-COMPOUND
-; RUN: opt < %s -passes=tsan -tsan-distinguish-volatile -tsan-compound-read-before-write -S | FileCheck %s --check-prefixes=CHECK,CHECK-COMPOUND-VOLATILE
+; RUN: opt < %s -passes=tsan -tsan-use-dominance-analysis=0 -S | FileCheck --check-prefixes=CHECK,CHECK-OPT %s
+; RUN: opt < %s -passes=tsan -tsan-instrument-read-before-write -tsan-use-dominance-analysis=0 -S | FileCheck %s --check-prefixes=CHECK,CHECK-UNOPT
+; RUN: opt < %s -passes=tsan -tsan-compound-read-before-write -tsan-use-dominance-analysis=0 -S | FileCheck %s --check-prefixes=CHECK,CHECK-COMPOUND
+; RUN: opt < %s -passes=tsan -tsan-distinguish-volatile -tsan-compound-read-before-write -tsan-use-dominance-analysis=0 -S | FileCheck %s --check-prefixes=CHECK,CHECK-COMPOUND-VOLATILE
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64-S128"
More information about the llvm-commits
mailing list