[clang] [Clang] Cache stable DeclContext relationships (NFC) (PR #223047)
Mehdi Amini via cfe-commits
cfe-commits at lists.llvm.org
Sat Sep 12 09:05:59 PDT 2026
https://github.com/joker-eph updated https://github.com/llvm/llvm-project/pull/223047
>From faa2ae401e324057d0690db5a5f104676bb3262f Mon Sep 17 00:00:00 2001
From: Mehdi Amini <joker.eph at gmail.com>
Date: Thu, 10 Sep 2026 05:51:53 -0700
Subject: [PATCH 1/2] Cache stable DeclContext relationships
Cache the owning AST context and stable C++ primary contexts after their
first traversal. Leave incomplete C++ records uncached. Resolve mutable
primary identities dynamically for namespaces, translation units,
C tags, and Objective-C: redeclaration linking, definition demotion,
and duplicate-definition merging can change their identities. Add
regression tests for these transitions.
The following measurements predate the cache-invalidation fixes above.
CTMark O0 (3 samples, CPU 6): 29.439800 s -> 29.239400 s (-0.681%).
Impact on significant TUs in MLIR build time:
- `mlir/lib/RegisterAllDialects.cpp`: 2.8987% fewer retired
instructions.
- `mlir/lib/Dialect/LLVMIR/IR/NVVMDialect.cpp`: 1.4486% fewer retired
instructions.
Assisted-by: Codex
---
clang/include/clang/AST/DeclBase.h | 26 +++++++++-
clang/lib/AST/DeclBase.cpp | 50 +++++++++++++++----
clang/unittests/AST/DeclTest.cpp | 78 ++++++++++++++++++++++++++++++
3 files changed, 143 insertions(+), 11 deletions(-)
diff --git a/clang/include/clang/AST/DeclBase.h b/clang/include/clang/AST/DeclBase.h
index 9d233be282dbb..1a2b1c72a6159 100644
--- a/clang/include/clang/AST/DeclBase.h
+++ b/clang/include/clang/AST/DeclBase.h
@@ -33,6 +33,7 @@
#include "llvm/Support/PrettyStackTrace.h"
#include "llvm/Support/VersionTuple.h"
#include <algorithm>
+#include <atomic>
#include <cassert>
#include <cstddef>
#include <iterator>
@@ -2101,6 +2102,17 @@ class DeclContext {
/// another pointer.
mutable Decl *LastDecl = nullptr;
+ /// A primary context whose identity cannot change. Incomplete C++ records
+ /// and contexts with mutable primary identities remain uncached.
+ /// Relaxed atomics permit cache population during concurrent read-only AST
+ /// traversal; they do not synchronize mutations to the AST itself.
+ mutable std::atomic<DeclContext *> CachedPrimaryContext = nullptr;
+
+ /// The owning AST context, which remains the same for this context's
+ /// lifetime. Null until first queried; relaxed access has the same contract
+ /// as above.
+ mutable std::atomic<ASTContext *> CachedASTContext = nullptr;
+
/// Build up a chain of declarations.
///
/// \returns the first/last pair of declarations.
@@ -2153,7 +2165,9 @@ class DeclContext {
}
ASTContext &getParentASTContext() const {
- return cast<Decl>(this)->getASTContext();
+ if (ASTContext *Cached = CachedASTContext.load(std::memory_order_relaxed))
+ return *Cached;
+ return getParentASTContextSlow();
}
bool isClosure() const { return getDeclKind() == Decl::Block; }
@@ -2288,7 +2302,12 @@ class DeclContext {
/// a different set of declarations. This routine returns the
/// "primary" DeclContext structure, which will contain the
/// information needed to perform name lookup into this context.
- DeclContext *getPrimaryContext();
+ DeclContext *getPrimaryContext() {
+ if (DeclContext *Cached =
+ CachedPrimaryContext.load(std::memory_order_relaxed))
+ return Cached;
+ return getPrimaryContextSlow();
+ }
const DeclContext *getPrimaryContext() const {
return const_cast<DeclContext*>(this)->getPrimaryContext();
}
@@ -2813,6 +2832,9 @@ class DeclContext {
StoredDeclsMap *CreateStoredDeclsMap(ASTContext &C) const;
+ ASTContext &getParentASTContextSlow() const;
+ DeclContext *getPrimaryContextSlow();
+
void loadLazyLocalLexicalLookups();
void buildLookupImpl(DeclContext *DCtx, bool Internal);
void makeDeclVisibleInContextWithFlags(NamedDecl *D, bool Internal,
diff --git a/clang/lib/AST/DeclBase.cpp b/clang/lib/AST/DeclBase.cpp
index 70f61fa57a682..59ddc64cf30e0 100644
--- a/clang/lib/AST/DeclBase.cpp
+++ b/clang/lib/AST/DeclBase.cpp
@@ -1488,7 +1488,20 @@ DeclContext *DeclContext::getNonTransparentContext() {
return DC;
}
-DeclContext *DeclContext::getPrimaryContext() {
+ASTContext &DeclContext::getParentASTContextSlow() const {
+ const DeclContext *DC = this;
+ while (!DC->isTranslationUnit()) {
+ DC = DC->getParent();
+ assert(DC && "This decl context is not contained in a translation unit!");
+ }
+
+ ASTContext &Context = cast<TranslationUnitDecl>(DC)->getASTContext();
+ CachedASTContext.store(&Context, std::memory_order_relaxed);
+ return Context;
+}
+
+DeclContext *DeclContext::getPrimaryContextSlow() {
+ DeclContext *Primary;
switch (getDeclKind()) {
case Decl::ExternCContext:
case Decl::LinkageSpec:
@@ -1502,7 +1515,8 @@ DeclContext *DeclContext::getPrimaryContext() {
case Decl::RequiresExprBody:
case Decl::CXXExpansionStmt:
// There is only one DeclContext for these entities.
- return this;
+ Primary = this;
+ break;
case Decl::HLSLBuffer:
// Each buffer, even with the same name, is a distinct construct.
@@ -1511,17 +1525,22 @@ DeclContext *DeclContext::getPrimaryContext() {
// As long as buffers have unique resource bindings the names don't matter.
// The names get exposed via the CPU-side reflection API which
// supports querying bindings, so we cannot remove them.
- return this;
+ Primary = this;
+ break;
case Decl::TranslationUnit:
+ // Redeclaration chains can change during AST merging. These queries are
+ // already constant-time, so do not cache their results.
return static_cast<TranslationUnitDecl *>(this)->getFirstDecl();
case Decl::Namespace:
return static_cast<NamespaceDecl *>(this)->getFirstDecl();
case Decl::ObjCMethod:
- return this;
+ Primary = this;
+ break;
case Decl::ObjCInterface:
+ // Duplicate-definition comparison temporarily changes the definition.
if (auto *OID = dyn_cast<ObjCInterfaceDecl>(this))
if (auto *Def = OID->getDefinition())
return Def;
@@ -1534,27 +1553,40 @@ DeclContext *DeclContext::getPrimaryContext() {
return this;
case Decl::ObjCCategory:
- return this;
+ Primary = this;
+ break;
case Decl::ObjCImplementation:
case Decl::ObjCCategoryImpl:
- return this;
+ Primary = this;
+ break;
// If this is a tag type that has a definition or is currently
// being defined, that definition is our primary context.
case Decl::ClassTemplatePartialSpecialization:
case Decl::ClassTemplateSpecialization:
- case Decl::CXXRecord:
- return cast<CXXRecordDecl>(this)->getDefinitionOrSelf();
+ case Decl::CXXRecord: {
+ CXXRecordDecl *Definition = cast<CXXRecordDecl>(this)->getDefinition();
+ if (!Definition)
+ return this;
+ Primary = Definition;
+ break;
+ }
case Decl::Record:
case Decl::Enum:
+ // Unlike C++ definition data, these definitions can be demoted during
+ // module merging, changing which declaration is the primary context.
return cast<TagDecl>(this)->getDefinitionOrSelf();
default:
assert(getDeclKind() >= Decl::firstFunction &&
getDeclKind() <= Decl::lastFunction && "Unknown DeclContext kind");
- return this;
+ Primary = this;
+ break;
}
+
+ CachedPrimaryContext.store(Primary, std::memory_order_relaxed);
+ return Primary;
}
template <typename T>
diff --git a/clang/unittests/AST/DeclTest.cpp b/clang/unittests/AST/DeclTest.cpp
index 195b8ab4c4e66..4048c34edf142 100644
--- a/clang/unittests/AST/DeclTest.cpp
+++ b/clang/unittests/AST/DeclTest.cpp
@@ -37,6 +37,84 @@ using namespace clang::ast_matchers;
using namespace clang::tooling;
using namespace clang;
+TEST(Decl, PrimaryContextAfterNamespaceRedeclaration) {
+ auto AST = tooling::buildASTFromCode("");
+ ASTContext &Ctx = AST->getASTContext();
+ auto *TU = Ctx.getTranslationUnitDecl();
+ auto *First = NamespaceDecl::Create(Ctx, TU, false, {}, {},
+ &Ctx.Idents.get("N"), nullptr, false);
+ auto *Second = NamespaceDecl::Create(Ctx, TU, false, {}, {},
+ &Ctx.Idents.get("N"), nullptr, false);
+ // A primary-context query must not freeze the redeclaration chain.
+ EXPECT_EQ(Second, Second->getPrimaryContext());
+ Second->setPreviousDecl(First);
+ EXPECT_EQ(First, Second->getPrimaryContext());
+ EXPECT_EQ(&Ctx, &Second->getParentASTContext());
+}
+
+TEST(Decl, PrimaryContextAfterDefinitionDemotion) {
+ auto AST = tooling::buildASTFromCodeWithArgs("struct S; struct S {};", {},
+ "input.c");
+ ASTContext &Ctx = AST->getASTContext();
+ auto *Forward = const_cast<RecordDecl *>(selectFirst<RecordDecl>(
+ "s",
+ match(recordDecl(hasName("S"), unless(isDefinition())).bind("s"), Ctx)));
+ ASSERT_NE(nullptr, Forward);
+ auto *Definition = Forward->getDefinition();
+ ASSERT_NE(nullptr, Definition);
+ EXPECT_EQ(Definition, Forward->getPrimaryContext());
+ Definition->demoteThisDefinitionToDeclaration();
+ EXPECT_EQ(Forward, Forward->getPrimaryContext());
+}
+
+TEST(Decl, PrimaryContextAfterObjCDuplicateDefinition) {
+ auto AST = tooling::buildASTFromCodeWithArgs("@protocol P @end",
+ {"-x", "objective-c"});
+ ASTContext &Ctx = AST->getASTContext();
+ auto *First = const_cast<ObjCProtocolDecl *>(selectFirst<ObjCProtocolDecl>(
+ "p", match(objcProtocolDecl(hasName("P")).bind("p"), Ctx)));
+ ASSERT_NE(nullptr, First);
+ auto *Duplicate = ObjCProtocolDecl::Create(
+ Ctx, Ctx.getTranslationUnitDecl(), &Ctx.Idents.get("P"), {}, {}, First);
+ Duplicate->startDuplicateDefinitionForComparison();
+ EXPECT_EQ(Duplicate, Duplicate->getPrimaryContext());
+ Duplicate->mergeDuplicateDefinitionWithCommon(First);
+ EXPECT_EQ(First, Duplicate->getPrimaryContext());
+}
+
+TEST(Decl, PrimaryContextAfterEnumDefinitionDemotion) {
+ auto AST = tooling::buildASTFromCodeWithArgs("enum E; enum E { Value };", {},
+ "input.c");
+ ASSERT_NE(nullptr, AST);
+ ASTContext &Ctx = AST->getASTContext();
+ auto *Forward = const_cast<EnumDecl *>(selectFirst<EnumDecl>(
+ "e",
+ match(enumDecl(hasName("E"), unless(isDefinition())).bind("e"), Ctx)));
+ ASSERT_NE(nullptr, Forward);
+ auto *Definition = Forward->getDefinition();
+ ASSERT_NE(nullptr, Definition);
+ EXPECT_EQ(Definition, Forward->getPrimaryContext());
+ Definition->demoteThisDefinitionToDeclaration();
+ EXPECT_EQ(Forward, Forward->getPrimaryContext());
+}
+
+TEST(Decl, PrimaryContextAfterObjCInterfaceDuplicateDefinition) {
+ auto AST = tooling::buildASTFromCodeWithArgs("@interface I @end",
+ {"-x", "objective-c"});
+ ASSERT_NE(nullptr, AST);
+ ASTContext &Ctx = AST->getASTContext();
+ auto *First = const_cast<ObjCInterfaceDecl *>(selectFirst<ObjCInterfaceDecl>(
+ "i", match(objcInterfaceDecl(hasName("I")).bind("i"), Ctx)));
+ ASSERT_NE(nullptr, First);
+ auto *Duplicate =
+ ObjCInterfaceDecl::Create(Ctx, Ctx.getTranslationUnitDecl(), {},
+ &Ctx.Idents.get("I"), nullptr, First);
+ Duplicate->startDuplicateDefinitionForComparison();
+ EXPECT_EQ(Duplicate, Duplicate->getPrimaryContext());
+ Duplicate->mergeDuplicateDefinitionWithCommon(First);
+ EXPECT_EQ(First, Duplicate->getPrimaryContext());
+}
+
TEST(Decl, CleansUpAPValues) {
MatchFinder Finder;
std::unique_ptr<FrontendActionFactory> Factory(
>From b8d9565e07e67d62103c445c5dced16db4eae8dc Mon Sep 17 00:00:00 2001
From: Mehdi Amini <joker.eph at gmail.com>
Date: Sat, 12 Sep 2026 18:05:35 +0200
Subject: [PATCH 2/2] [Clang] Do not imply concurrent AST traversal support
---
clang/include/clang/AST/DeclBase.h | 15 +++++----------
clang/lib/AST/DeclBase.cpp | 4 ++--
2 files changed, 7 insertions(+), 12 deletions(-)
diff --git a/clang/include/clang/AST/DeclBase.h b/clang/include/clang/AST/DeclBase.h
index 1a2b1c72a6159..55dbda3d4d0c0 100644
--- a/clang/include/clang/AST/DeclBase.h
+++ b/clang/include/clang/AST/DeclBase.h
@@ -33,7 +33,6 @@
#include "llvm/Support/PrettyStackTrace.h"
#include "llvm/Support/VersionTuple.h"
#include <algorithm>
-#include <atomic>
#include <cassert>
#include <cstddef>
#include <iterator>
@@ -2104,14 +2103,11 @@ class DeclContext {
/// A primary context whose identity cannot change. Incomplete C++ records
/// and contexts with mutable primary identities remain uncached.
- /// Relaxed atomics permit cache population during concurrent read-only AST
- /// traversal; they do not synchronize mutations to the AST itself.
- mutable std::atomic<DeclContext *> CachedPrimaryContext = nullptr;
+ mutable DeclContext *CachedPrimaryContext = nullptr;
/// The owning AST context, which remains the same for this context's
- /// lifetime. Null until first queried; relaxed access has the same contract
- /// as above.
- mutable std::atomic<ASTContext *> CachedASTContext = nullptr;
+ /// lifetime. Null until first queried.
+ mutable ASTContext *CachedASTContext = nullptr;
/// Build up a chain of declarations.
///
@@ -2165,7 +2161,7 @@ class DeclContext {
}
ASTContext &getParentASTContext() const {
- if (ASTContext *Cached = CachedASTContext.load(std::memory_order_relaxed))
+ if (ASTContext *Cached = CachedASTContext)
return *Cached;
return getParentASTContextSlow();
}
@@ -2303,8 +2299,7 @@ class DeclContext {
/// "primary" DeclContext structure, which will contain the
/// information needed to perform name lookup into this context.
DeclContext *getPrimaryContext() {
- if (DeclContext *Cached =
- CachedPrimaryContext.load(std::memory_order_relaxed))
+ if (DeclContext *Cached = CachedPrimaryContext)
return Cached;
return getPrimaryContextSlow();
}
diff --git a/clang/lib/AST/DeclBase.cpp b/clang/lib/AST/DeclBase.cpp
index 59ddc64cf30e0..e54d4757f41f4 100644
--- a/clang/lib/AST/DeclBase.cpp
+++ b/clang/lib/AST/DeclBase.cpp
@@ -1496,7 +1496,7 @@ ASTContext &DeclContext::getParentASTContextSlow() const {
}
ASTContext &Context = cast<TranslationUnitDecl>(DC)->getASTContext();
- CachedASTContext.store(&Context, std::memory_order_relaxed);
+ CachedASTContext = &Context;
return Context;
}
@@ -1585,7 +1585,7 @@ DeclContext *DeclContext::getPrimaryContextSlow() {
break;
}
- CachedPrimaryContext.store(Primary, std::memory_order_relaxed);
+ CachedPrimaryContext = Primary;
return Primary;
}
More information about the cfe-commits
mailing list