Skip to content

Commit c79aa2e

Browse files
committed
Do not clone reverse-mode leaf representations until read.
A reverse-mode StmtDiff carries three representations of a value -- forward, derivative, and reverse-sweep. The leaf visitors (VisitDeclRefExpr, VisitIntegerLiteral, VisitFloatingLiteral) built all three eagerly, cloning the underlying node once per representation. But most parents read only a subset: a terminal product-rule leaf, having emitted its own increment, never reads its adjoint, and a value whose primal is not reconstructed never reads its forward. The unread clones are allocated in the ASTContext and never parented -- wasted memory. Give StmtDiff optional lazy representations: a slot may hold a deferred clone of a source node, produced (and cached) only when that representation is first read, written as LazyClone(N) in a constructor argument. A representation no consumer reads is never cloned; two representations off the same source still materialize distinct nodes, so the no-shared-node invariant (findSharedNode) is preserved. Return the reverse-mode DeclRef and literal leaves through LazyClone, and drop the separate eager reverse-sweep copy, which defaults to the forward node. The full lit suite is unchanged.
1 parent 49dc18e commit c79aa2e

3 files changed

Lines changed: 121 additions & 25 deletions

File tree

include/clad/Differentiator/VisitorBase.h

Lines changed: 96 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,42 +51,114 @@ namespace clad {
5151
/// other (intermediate) statements, they are output to the current block.
5252
class StmtDiff {
5353
private:
54-
std::array<clang::Stmt*, 2> data;
54+
std::array<clang::Stmt*, 2> m_Data{};
5555
clang::Stmt* m_ValueForRevSweep;
5656

57+
// Lazy representations. When a source is set (via LazyClone() in a
58+
// constructor argument), the matching slot is produced by cloning the
59+
// source on first read and then cached, so a representation no consumer
60+
// reads is never cloned (no orphaned "dead clone"), while two
61+
// representations off the same source still materialize distinct nodes (no
62+
// sharing). An unset source means the slot is eager -- the stored pointer
63+
// (possibly null) is the value -- the default for every non-lazy argument.
64+
const clang::Stmt* m_StmtSrc = nullptr;
65+
const clang::Stmt* m_StmtDxSrc = nullptr;
66+
const clang::Stmt* m_RevSweepSrc = nullptr;
67+
utils::StmtClone* m_Cloner = nullptr;
68+
69+
// Clone Src into Slot on first read; a no-op when Src is null (eager slot).
70+
clang::Stmt* materialize(clang::Stmt*& Slot, const clang::Stmt*& Src);
71+
5772
public:
58-
StmtDiff(clang::Stmt* orig = nullptr, clang::Stmt* diff = nullptr,
59-
clang::Stmt* valueForRevSweep = nullptr)
60-
: m_ValueForRevSweep(valueForRevSweep) {
61-
data[1] = orig;
62-
data[0] = diff;
73+
/// A deferred clone marker: a representation cloned from Src on first read,
74+
/// created by VisitorBase::LazyClone(). No member initializers, so In
75+
/// (which embeds it) can be a `= {}` default argument while StmtDiff is
76+
/// still being defined.
77+
struct Lazy {
78+
utils::StmtClone* Cloner;
79+
const clang::Stmt* Src;
80+
};
81+
82+
/// A constructor input for one representation: either an already-built node
83+
/// (eager -- the node itself is the value) or a Lazy deferred clone. Node
84+
/// and Deferred are set in the constructors rather than by default member
85+
/// initializers, so In can be used as a `= {}` default argument here.
86+
struct In {
87+
clang::Stmt* Node;
88+
Lazy Deferred;
89+
In(clang::Stmt* N = nullptr) : Node(N), Deferred{nullptr, nullptr} {}
90+
In(Lazy L) : Node(nullptr), Deferred(L) {}
91+
};
92+
93+
/// Implicit single-representation constructor. Keeps the Expr*/Stmt* ->
94+
/// StmtDiff conversion pervasive code relies on (return expr; sd = expr;),
95+
/// which needs one user-defined conversion -- reaching the general
96+
/// constructor below through In(Stmt*) would need two.
97+
StmtDiff(clang::Stmt* orig) : m_ValueForRevSweep(nullptr) {
98+
m_Data[1] = orig;
99+
m_Data[0] = nullptr;
100+
}
101+
102+
/// General constructor: each representation is eager (a node, the default
103+
/// for every existing multi-argument call site) or lazy (LazyClone(src)).
104+
/// Reads e.g. StmtDiff(fwd, LazyClone(dx)) or StmtDiff(LazyClone(fwd), dx).
105+
StmtDiff(In orig = {}, In diff = {}, In valueForRevSweep = {})
106+
: m_ValueForRevSweep(valueForRevSweep.Node),
107+
m_StmtSrc(orig.Deferred.Src), m_StmtDxSrc(diff.Deferred.Src),
108+
m_RevSweepSrc(valueForRevSweep.Deferred.Src),
109+
// Every lazy slot shares the one cloner (VisitorBase::m_NodeCloner);
110+
// take the first non-null.
111+
m_Cloner([&] {
112+
if (orig.Deferred.Cloner)
113+
return orig.Deferred.Cloner;
114+
if (diff.Deferred.Cloner)
115+
return diff.Deferred.Cloner;
116+
return valueForRevSweep.Deferred.Cloner;
117+
}()) {
118+
m_Data[1] = orig.Node;
119+
m_Data[0] = diff.Node;
63120
}
64121

65-
clang::Stmt* getStmt() { return data[1]; }
66-
clang::Stmt* getStmt_dx() { return data[0]; }
122+
clang::Stmt* getStmt() { return materialize(m_Data[1], m_StmtSrc); }
123+
clang::Stmt* getStmt_dx() { return materialize(m_Data[0], m_StmtDxSrc); }
67124
clang::Expr* getExpr() {
68125
return llvm::cast_or_null<clang::Expr>(getStmt());
69126
}
70127
clang::Expr* getExpr_dx() {
71128
return llvm::cast_or_null<clang::Expr>(getStmt_dx());
72129
}
73130

74-
void updateStmt(clang::Stmt* S) { data[1] = S; }
75-
void updateStmtDx(clang::Stmt* S) { data[0] = S; }
76-
void updateRevSweep(clang::Stmt* S) { m_ValueForRevSweep = S; }
131+
void updateStmt(clang::Stmt* S) {
132+
m_Data[1] = S;
133+
m_StmtSrc = nullptr;
134+
}
135+
void updateStmtDx(clang::Stmt* S) {
136+
m_Data[0] = S;
137+
m_StmtDxSrc = nullptr;
138+
}
139+
void updateRevSweep(clang::Stmt* S) {
140+
m_ValueForRevSweep = S;
141+
m_RevSweepSrc = nullptr;
142+
}
77143
// Stmt_dx goes first!
78-
std::array<clang::Stmt*, 2>& getBothStmts() { return data; }
144+
std::array<clang::Stmt*, 2>& getBothStmts() {
145+
// A caller taking the array by reference parents both directions, so
146+
// materialize both.
147+
getStmt();
148+
getStmt_dx();
149+
return m_Data;
150+
}
79151

80152
clang::Expr* getRevSweepAsExpr() {
81153
return llvm::cast_or_null<clang::Expr>(getRevSweepStmt());
82154
}
83155

84156
clang::Stmt* getRevSweepStmt() {
85-
/// If there is no specific value for
86-
/// the reverse sweep, use Stmt_dx.
87-
if (!m_ValueForRevSweep)
88-
return data[1];
89-
return m_ValueForRevSweep;
157+
if (clang::Stmt* R = materialize(m_ValueForRevSweep, m_RevSweepSrc))
158+
return R;
159+
// If there is no specific value for the reverse sweep, use the forward
160+
// statement.
161+
return getStmt();
90162
}
91163
};
92164

@@ -703,6 +775,13 @@ namespace clad {
703775
clang::Expr* CloneNode(const clang::Expr* E);
704776
/// Statement overload of the structural copy above.
705777
clang::Stmt* CloneNode(const clang::Stmt* S);
778+
/// A deferred CloneNode(\p N): the clone is produced only if the StmtDiff
779+
/// representation it is stored in is actually read, so a representation no
780+
/// consumer needs allocates no orphaned clone. Drop-in for CloneNode(N) in
781+
/// a StmtDiff argument position.
782+
StmtDiff::Lazy LazyClone(const clang::Stmt* N) {
783+
return {m_Builder.m_NodeCloner.get(), N};
784+
}
706785
/// Cloning types is necessary since VariableArrayType
707786
/// store a pointer to their size expression.
708787
clang::QualType CloneType(clang::QualType T);

lib/Differentiator/ReverseModeVisitor.cpp

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,24 +1589,30 @@ Expr* ReverseModeVisitor::getStdInitListSizeExpr(const Expr* E) {
15891589
// dfdx) statement.
15901590
if (Expr* add_assign = BuildDiffIncrement(CloneNode(dExpr)))
15911591
addToCurrentBlock(add_assign, direction::reverse);
1592-
// getExpr (forward rebuild) and the reverse-sweep value are consumed by
1593-
// different statements, so hand out distinct clones of the primal ref.
1594-
return StmtDiff(clonedDRE, CloneNode(dExpr), CloneNode(clonedDRE));
1592+
// The adjoint (getExpr_dx, e.g. *_d_x) is read only by parents that
1593+
// propagate a subexpression's derivative -- a conditional, a paren, an
1594+
// array-subscript base; a terminal product-rule leaf, having already
1595+
// emitted its own increment above, never reads it. Clone it lazily so
1596+
// those terminal leaves allocate no orphaned copy, while a consumer still
1597+
// materializes a distinct node (no sharing). The reverse-sweep value
1598+
// defaults to the forward node.
1599+
return StmtDiff(clonedDRE, LazyClone(dExpr));
15951600
}
15961601

1597-
return StmtDiff(clonedDRE, /*diff=*/nullptr, CloneNode(clonedDRE));
1602+
return StmtDiff(clonedDRE);
15981603
}
15991604

16001605
StmtDiff ReverseModeVisitor::VisitIntegerLiteral(const IntegerLiteral* IL) {
16011606
auto* Constant0 =
16021607
ConstantFolder::synthesizeLiteral(m_Context.IntTy, m_Context, 0);
1603-
// Distinct forward and reverse-sweep copies so a parent consuming both
1604-
// does not share the literal node.
1605-
return StmtDiff(Clone(IL), Constant0, Clone(IL));
1608+
// The forward value (also the reverse-sweep default) is a copy of the
1609+
// literal, needed only if a parent reads it; clone it lazily so a leaf no
1610+
// parent reads allocates nothing. The derivative of a constant is 0.
1611+
return StmtDiff(LazyClone(IL), Constant0);
16061612
}
16071613

16081614
StmtDiff ReverseModeVisitor::VisitFloatingLiteral(const FloatingLiteral* FL) {
1609-
return StmtDiff(Clone(FL), getZeroInit(FL->getType()), Clone(FL));
1615+
return StmtDiff(LazyClone(FL), getZeroInit(FL->getType()));
16101616
}
16111617

16121618
static bool isNAT(QualType T) {

lib/Differentiator/VisitorBase.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,17 @@ namespace clad {
424424
return S ? m_Builder.m_NodeCloner->Clone(S) : nullptr;
425425
}
426426

427+
clang::Stmt* StmtDiff::materialize(clang::Stmt*& Slot,
428+
const clang::Stmt*& Src) {
429+
if (!Slot && Src && m_Cloner) {
430+
Slot = m_Cloner->Clone(Src);
431+
// The representation is now cached; drop the recipe so later reads (and
432+
// identity/null checks) return the same node.
433+
Src = nullptr;
434+
}
435+
return Slot;
436+
}
437+
427438
QualType VisitorBase::CloneType(const QualType QT) {
428439
auto clonedType = m_Builder.m_NodeCloner->CloneType(QT);
429440
utils::ReferencesUpdater up(m_Sema, getCurrentScope(), m_DiffReq.Function,

0 commit comments

Comments
 (0)