Jlm
Loading...
Searching...
No Matches
LocalAliasAnalysis.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2025 HÃ¥vard Krogstie <krogstie.havard@gmail.com>
3 * See COPYING for terms of redistribution.
4 */
5
14#include <jlm/llvm/ir/Trace.hpp>
15#include <jlm/llvm/ir/types.hpp>
17#include <jlm/rvsdg/gamma.hpp>
18#include <jlm/rvsdg/lambda.hpp>
20#include <jlm/rvsdg/theta.hpp>
21
22#include <queue>
23#include <variant>
24
25namespace jlm::llvm::aa
26{
27
29
31
32std::string
34{
35 return "LocalAA";
36}
37
38size_t
43
44void
46{
47 maxTraceCollectionSize_ = maxTraceCollectionSize;
48}
49
51LocalAliasAnalysis::Query(const rvsdg::Output & p1, size_t s1, const rvsdg::Output & p2, size_t s2)
52{
53 const auto & p1Norm = llvm::traceOutput(p1);
54 const auto & p2Norm = llvm::traceOutput(p2);
55
56 // If the two pointers are the same value, they must alias
57 if (&p1Norm == &p2Norm)
58 return MustAlias;
59
60 // Trace through GEP operations to get closer to the origins of the pointers
61 // Only trace through GEPs where the offset is known at compile time,
62 // to avoid giving up on MustAlias prematurely
63 const auto p1Traced = TracePointerOriginPrecise(p1Norm);
64 const auto p2Traced = TracePointerOriginPrecise(p2Norm);
65 auto p1OffsetInBytesOpt = p1Traced.getOffsetInBytes();
66 auto p2OffsetInBytesOpt = p2Traced.getOffsetInBytes();
67 JLM_ASSERT(p1OffsetInBytesOpt.has_value() && p2OffsetInBytesOpt.has_value());
68
69 if (p1Traced.BasePointer == p2Traced.BasePointer)
70 {
71 // The pointers share base, but may have different offsets
72 // p1 = base + p1Offset
73 // p2 = base + p2Offset
74
75 return QueryOffsets(p1OffsetInBytesOpt, s1, p2OffsetInBytesOpt, s2);
76 }
77
78 // If the max trace collection size is set to 1,
79 // give up before tracing through multiple origins or unknown offsets.
81 {
82 // If both pointers are original origins, and not the same pointer, we have NoAlias
83 if (IsOriginalOrigin(*p1Traced.BasePointer) && IsOriginalOrigin(*p2Traced.BasePointer))
84 return NoAlias;
85
86 return MayAlias;
87 }
88
89 // Keep tracing back to all sources
90 TraceCollection p1TraceCollection;
91 TraceCollection p2TraceCollection;
92
93 // If tracing reaches too many possible outputs, it may give up
94 if (!TraceAllPointerOrigins(p1Traced, p1TraceCollection, maxTraceCollectionSize_))
95 return MayAlias;
96
97 if (!TraceAllPointerOrigins(p2Traced, p2TraceCollection, maxTraceCollectionSize_))
98 return MayAlias;
99
100 // Removes top origins that can not possibly be valid targets due to being too small.
101 // If p1 + s1 is outside the range of a top origin, then p1 can not target it
102 RemoveTopOriginsWithRemainingSizeBelow(p1TraceCollection, s1);
103 RemoveTopOriginsWithRemainingSizeBelow(p2TraceCollection, s2);
104
105 // If each trace collection has only one top origin, check if they have the same base pointer
106 if (p1TraceCollection.TopOrigins.size() == 1 && p2TraceCollection.TopOrigins.size() == 1)
107 {
108 const auto & [p1Base, p1Offset] = *p1TraceCollection.TopOrigins.begin();
109 const auto & [p2Base, p2Offset] = *p2TraceCollection.TopOrigins.begin();
110 if (p1Base == p2Base)
111 return QueryOffsets(p1Offset, s1, p2Offset, s2);
112 }
113
114 // From this point on we give up on MustAlias
115
116 // Since we only have inbound GEPs, a pointer p = b + 12 must point at least 12 bytes into
117 // the memory region it points to
118 auto minimumP1OffsetFromStart = GetMinimumOffsetFromStart(p1TraceCollection);
119 auto minimumP2OffsetFromStart = GetMinimumOffsetFromStart(p2TraceCollection);
120
121 // In case the trace collections contain unknown offsets, also try using the
122 // precise p1Traced and p2Traced, which always have a known offset
123 if (*p1OffsetInBytesOpt > 0)
124 minimumP1OffsetFromStart =
125 std::max(minimumP1OffsetFromStart, static_cast<size_t>(*p1OffsetInBytesOpt));
126 if (*p2OffsetInBytesOpt > 0)
127 minimumP2OffsetFromStart =
128 std::max(minimumP2OffsetFromStart, static_cast<size_t>(*p2OffsetInBytesOpt));
129
130 // Since we have given up on MustAlias, we can remove some targets even if they are valid.
131 // Even if p1 might point to an 4-byte int foo,
132 // if p2 is an 8-byte operation, or p2 is at least 4 bytes into its target,
133 // we can safely discard that p1 might target foo.
134 RemoveTopOriginsSmallerThanSize(p2TraceCollection, minimumP1OffsetFromStart + s1);
135 RemoveTopOriginsSmallerThanSize(p1TraceCollection, minimumP2OffsetFromStart + s2);
136
137 // If we know that p2 is at least 12 bytes into the memory region it targets,
138 // then any use of p1 where p1 + s1 is within the first 12 bytes of its memory region can be
139 // ignored.
140 RemoveTopOriginsWithinTheFirstNBytes(p1TraceCollection, s1, minimumP2OffsetFromStart);
141 RemoveTopOriginsWithinTheFirstNBytes(p2TraceCollection, s2, minimumP1OffsetFromStart);
142
143 // Any direct overlap in the collections' top sets means there is a possibility of aliasing
144 if (DoTraceCollectionsOverlap(p1TraceCollection, s1, p2TraceCollection, s2))
145 return MayAlias;
146
147 // Even if there is no direct overlap in the trace collections, the pointers may still alias
148 // Take for example the top sets { ALLOCA[a]+40, ALLOCA[b] } and { ALLOCA[c], o4+20 }
149 // o4 is some output that can not be traced further, but it is also not original.
150 // It is possible for o4 to be a pointer to ALLOCA[a]+20, in which case there is aliasing.
151
152 // We already know that there is no direct overlap in the top origin sets,
153 // so if both trace collections only contain original pointers, there is NoAlias.
154 const bool p1AllTopsOriginal = HasOnlyOriginalTopOrigins(p1TraceCollection);
155 const bool p2AllTopsOriginal = HasOnlyOriginalTopOrigins(p2TraceCollection);
156
157 if (p1AllTopsOriginal && p2AllTopsOriginal)
158 return NoAlias;
159
160 // If all top origins of a pointer are ALLOCAs, then the other pointer can discard all origins
161 // that are from arguments to the function
162 if (hasOnlyAllocaTopOrigins(p1TraceCollection))
163 removeArgumentTopOrigins(p2TraceCollection);
164 else if (hasOnlyAllocaTopOrigins(p2TraceCollection))
165 removeArgumentTopOrigins(p1TraceCollection);
166
167 // If one of the trace collections are empty, they can not alias
168 if (p1TraceCollection.TopOrigins.empty() || p2TraceCollection.TopOrigins.empty())
169 return NoAlias;
170
171 // If one of the pointers has a top origin set containing only fully traceable ALLOCAs,
172 // it is not possible for the other pointer to target any of them,
173 // as they would already be explicitly included in its top origin set.
174 const bool p1OnlyTraceable = HasOnlyFullyTraceableTopOrigins(p1TraceCollection);
175 if (p1OnlyTraceable)
176 return NoAlias;
177
178 const bool p2OnlyTraceable = HasOnlyFullyTraceableTopOrigins(p2TraceCollection);
179 if (p2OnlyTraceable)
180 return NoAlias;
181
182 return MayAlias;
183}
184
187 std::optional<int64_t> offset1,
188 size_t s1,
189 std::optional<int64_t> offset2,
190 size_t s2)
191{
192 // If either offset is unknown, return MayAlias
193 if (!offset1.has_value() || !offset2.has_value())
194 return MayAlias;
195
196 auto difference = *offset2 - *offset1;
197 if (difference == 0)
198 return MustAlias;
199
200 // p2 starts at or after p1+s1
201 if (difference >= 0 && static_cast<size_t>(difference) >= s1)
202 return NoAlias;
203
204 // p1 starts at or after p2+s2
205 if (difference <= 0 && static_cast<size_t>(-difference) >= s2)
206 return NoAlias;
207
208 // We have a partial alias
209 return MayAlias;
210}
211
212bool
214{
215 // Each GraphImport represents a unique object
216 if (dynamic_cast<const LlvmGraphImport *>(&pointer))
217 return true;
218
220 return true;
221
223 return true;
224
225 // Is pointer the output of one of the nodes
226 if (const auto node = rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(pointer))
227 {
228 if (is<AllocaOperation>(node->GetOperation()))
229 return true;
230
231 if (is<MallocOperation>(node->GetOperation()))
232 return true;
233 }
234
235 return false;
236}
237
238bool
240{
241 for (auto [output, offset] : traces.TopOrigins)
242 {
243 if (!IsOriginalOrigin(*output))
244 return false;
245 }
246 return true;
247}
248
249std::optional<size_t>
251{
252 if (auto delta = rvsdg::TryGetOwnerNode<rvsdg::DeltaNode>(pointer))
253 return GetTypeAllocSize(*delta->GetOperation().Type());
254 if (auto import = dynamic_cast<const LlvmGraphImport *>(&pointer))
255 {
256 auto size = GetTypeAllocSize(*import->ValueType());
257 // Workaround for imported incomplete types appearing to have size 0 in the LLVM IR
258 if (size == 0)
259 return std::nullopt;
260
261 return size;
262 }
263 if (const auto [node, allocaOp] = rvsdg::TryGetSimpleNodeAndOptionalOp<AllocaOperation>(pointer);
264 allocaOp)
265 {
266 const auto elementCount = tryGetConstantSignedInteger(*node->input(0)->origin());
267 if (elementCount.has_value())
268 return *elementCount * GetTypeAllocSize(*allocaOp->allocatedType());
269 }
270 if (const auto [node, mallocOp] = rvsdg::TryGetSimpleNodeAndOptionalOp<MallocOperation>(pointer);
271 mallocOp)
272 {
273 const auto mallocSize =
275 if (mallocSize.has_value())
276 return *mallocSize;
277 }
278
279 return std::nullopt;
280}
281
282std::optional<size_t>
284 const rvsdg::Output & basePointer,
285 const std::optional<int64_t> & offsetInBytes)
286{
287 const auto totalSize = GetOriginalOriginSize(basePointer);
288 if (!totalSize.has_value())
289 return std::nullopt;
290
291 if (!offsetInBytes.has_value())
292 return *totalSize;
293
294 // Avoid wrap-around by truncating remaining size to min 0
295 if (*offsetInBytes > 0 && static_cast<size_t>(*offsetInBytes) > *totalSize)
296 return 0;
297
298 return *totalSize - *offsetInBytes;
299}
300
301void
303{
304 auto it = traces.TopOrigins.begin();
305 while (it != traces.TopOrigins.end())
306 {
307 const auto remainingSize = GetRemainingSize(*it->first, it->second);
308 if (remainingSize.has_value())
309 {
310 // This top origin leaves too little room, and can be fully removed
311 if (*remainingSize < s)
312 {
313 it = traces.TopOrigins.erase(it);
314 continue;
315 }
316
317 // If a top origin is exactly large enough for s, any unknown offset must be 0
318 if (*remainingSize == s && !it->second.has_value())
319 it->second = 0;
320 }
321 it++;
322 }
323}
324
325size_t
327{
328 std::optional<size_t> minimumOffset;
329 for (auto [output, offset] : traces.TopOrigins)
330 {
331 // If one of the possible targets has an unknown offset, just use the access size
332 if (!offset.has_value())
333 return 0;
334
335 if (*offset < 0)
336 return 0;
337
338 if (minimumOffset.has_value())
339 minimumOffset = std::min(*minimumOffset, static_cast<size_t>(*offset));
340 else
341 minimumOffset = offset;
342 }
343
344 if (minimumOffset.has_value())
345 return *minimumOffset;
346
347 // We only get here if the top origins is empty, in which case the return value
348 // does not matter, as the query will return NoAlias anyway.
349 return 0;
350}
351
352void
354{
355 auto it = traces.TopOrigins.begin();
356 while (it != traces.TopOrigins.end())
357 {
358 auto originSize = GetOriginalOriginSize(*it->first);
359 if (originSize.has_value() && *originSize < s)
360 it = traces.TopOrigins.erase(it);
361 else
362 it++;
363 }
364}
365
366void
368 TraceCollection & traces,
369 size_t s,
370 size_t N)
371{
372 auto it = traces.TopOrigins.begin();
373 while (it != traces.TopOrigins.end())
374 {
375 const auto offset = it->second;
376
377 // If the pointer is original, it is also pointing to the beginning of the memory region.
378 // The offset thus tells us exactly which bytes within the memory region we touch.
379 if (IsOriginalOrigin(*it->first) && offset.has_value() && *offset + s <= N)
380 it = traces.TopOrigins.erase(it);
381 else
382 it++;
383 }
384}
385
386bool
388 TraceCollection & tc1,
389 size_t s1,
390 TraceCollection & tc2,
391 size_t s2)
392{
393 for (auto [p1Origin, p1Offset] : tc1.TopOrigins)
394 {
395 auto p2Find = tc2.TopOrigins.find(p1Origin);
396 if (p2Find == tc2.TopOrigins.end())
397 continue;
398
399 auto p2Offset = p2Find->second;
400 if (QueryOffsets(p1Offset, s1, p2Offset, s2) != NoAlias)
401 return true;
402 }
403
404 return false;
405}
406
407bool
409{
410 for (auto [output, offset] : traceCollection.TopOrigins)
411 {
413 return false;
414 }
415 return true;
416}
417
418void
420{
421 for (auto it = traceCollection.TopOrigins.begin(); it != traceCollection.TopOrigins.end();)
422 {
423 auto & output = *it->first;
425 if (lambda)
426 {
427 auto argument = lambda->MapArgument(output);
428 if (std::holds_alternative<rvsdg::LambdaNode::ArgumentVar>(argument))
429 {
430 // The output is a function argument, remove it
431 it = traceCollection.TopOrigins.erase(it);
432 continue;
433 }
434 }
435
436 // We did not remove this top origin, keep iterating
437 it++;
438 }
439}
440
441bool
443{
444 // The only original origins that can be fully traced for escaping are ALLOCAs
446 return false;
447
448 // Check if the result for this ALLOCA is already memoized
449 auto it = IsFullyTraceable_.find(&pointer);
450 if (it != IsFullyTraceable_.end())
451 return it->second;
452
453 // Use a queue to find all users of the ALLOCA's address
454 std::queue<const rvsdg::Output *> qu;
455 std::unordered_set<const rvsdg::Output *> added;
456
457 const auto Enqueue = [&](const rvsdg::Output & p)
458 {
459 // Only enqueue new outputs
460 auto [_, inserted] = added.insert(&p);
461 if (inserted)
462 qu.push(&p);
463 };
464
465 Enqueue(pointer);
466 while (!qu.empty())
467 {
468 auto & p = *qu.front();
469 qu.pop();
470
471 // Handle all inputs that are users of p
472 for (auto & user : p.Users())
473 {
474 if (auto gamma = rvsdg::TryGetOwnerNode<rvsdg::GammaNode>(user))
475 {
476 auto input = gamma->MapInput(user);
477
478 // A pointer must always be an EntryVar, as the MatchVar has a ControlType
479 auto entry = std::get_if<rvsdg::GammaNode::EntryVar>(&input);
480 JLM_ASSERT(entry);
481
482 for (auto output : entry->branchArgument)
483 Enqueue(*output);
484
485 continue;
486 }
488 {
489 // user is a gamma result, find the corresponding gamma output
490 auto exitVar = gamma->MapBranchResultExitVar(user);
491 Enqueue(*exitVar.output);
492
493 continue;
494 }
495
496 if (auto theta = rvsdg::TryGetOwnerNode<rvsdg::ThetaNode>(user))
497 {
498 auto loopVar = theta->MapInputLoopVar(user);
499
500 // The loop always runs at least once, so map it to the inside
501 Enqueue(*loopVar.pre);
502
503 continue;
504 }
506 {
507 // user is a theta result, find the corresponding loop variable
508 auto loopVar = theta->MapPostLoopVar(user);
509 Enqueue(*loopVar.pre);
510 Enqueue(*loopVar.output);
511
512 continue;
513 }
514
515 if (auto node = rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(user))
516 {
517 bool do_continue = MatchTypeWithDefault(
518 node->GetOperation(),
519 [&](const IOBarrierOperation &)
520 {
521 // The pointer input must be the node's first input
522 JLM_ASSERT(user.index() == 0);
523 Enqueue(*node->output(0));
524 return true;
525 },
526 [&](const GetElementPtrOperation &)
527 {
528 // The pointer input must be the node's first input
529 JLM_ASSERT(user.index() == 0);
530 Enqueue(*node->output(0));
531 return true;
532 },
533 [&](const SelectOperation &)
534 {
535 // Select operations are fine, if the output is still fully traceable
536 Enqueue(*node->output(0));
537 return true;
538 },
539 [&](const LoadOperation &)
540 {
541 // Loads are always fine
542 return true;
543 },
544 [&](const StoreOperation &)
545 {
546 // Stores are only fine if the pointer itself is not being stored somewhere
547 if (&user == &StoreOperation::AddressInput(*node))
548 return true;
549 else
550 return false;
551 },
552 []()
553 {
554 return false;
555 });
556 if (do_continue)
557 continue;
558 }
559
560 // We were unable to handle this user, so the original pointer escapes tracing
561 IsFullyTraceable_[&pointer] = false;
562 return false;
563 }
564 }
565
566 // The entire queue was processed without reaching a single untraceable user of the pointer
567 IsFullyTraceable_[&pointer] = true;
568 return true;
569}
570
571bool
573{
574 for (auto [topOrigin, _] : traces.TopOrigins)
575 {
576 if (!IsOriginalOriginFullyTraceable(*topOrigin))
577 return false;
578 }
579
580 return true;
581}
582
583}
static rvsdg::Input & sizeInput(const rvsdg::Node &node)
static void RemoveTopOriginsWithinTheFirstNBytes(TraceCollection &traces, size_t s, size_t N)
static bool IsOriginalOrigin(const rvsdg::Output &pointer)
~LocalAliasAnalysis() noexcept override
static bool hasOnlyAllocaTopOrigins(const TraceCollection &traceCollection)
bool HasOnlyFullyTraceableTopOrigins(TraceCollection &traces)
AliasQueryResponse Query(const rvsdg::Output &p1, size_t s1, const rvsdg::Output &p2, size_t s2) override
static std::optional< size_t > GetRemainingSize(const rvsdg::Output &basePointer, const std::optional< int64_t > &offsetInBytes)
static void removeArgumentTopOrigins(TraceCollection &traceCollection)
std::unordered_map< const rvsdg::Output *, bool > IsFullyTraceable_
static bool DoTraceCollectionsOverlap(TraceCollection &tc1, size_t s1, TraceCollection &tc2, size_t s2)
static void RemoveTopOriginsSmallerThanSize(TraceCollection &traces, size_t s)
void setMaxTraceCollectionSize(size_t maxTraceCollectionSize)
static bool HasOnlyOriginalTopOrigins(TraceCollection &traces)
static size_t GetMinimumOffsetFromStart(TraceCollection &traces)
bool IsOriginalOriginFullyTraceable(const rvsdg::Output &pointer)
static void RemoveTopOriginsWithRemainingSizeBelow(TraceCollection &traces, size_t s)
static AliasQueryResponse QueryOffsets(std::optional< int64_t > offset1, size_t s1, std::optional< int64_t > offset2, size_t s2)
static std::optional< size_t > GetOriginalOriginSize(const rvsdg::Output &pointer)
Output * origin() const noexcept
Definition node.hpp:58
#define JLM_ASSERT(x)
Definition common.hpp:16
size_t GetTypeAllocSize(const rvsdg::Type &type)
Definition types.cpp:473
bool TraceAllPointerOrigins(TracedPointerOrigin p, TraceCollection &traceCollection, const size_t maxTraceCollectionSize)
Definition Trace.cpp:326
rvsdg::Output & traceOutput(rvsdg::Output &output, const rvsdg::Region *withinRegion)
Definition Trace.cpp:62
TracedPointerOrigin TracePointerOriginPrecise(const rvsdg::Output &p)
Definition Trace.cpp:153
static std::string ToString(const std::vector< MemoryNodeId > &memoryNodeIds)
std::optional< int64_t > tryGetConstantSignedInteger(const rvsdg::Output &output)
Definition Trace.cpp:70
NodeType * TryGetOwnerNode(const rvsdg::Input &input) noexcept
Checks if this is an input to a node of specified type.
Definition node.hpp:872
std::unordered_map< const rvsdg::Output *, std::optional< int64_t > > TopOrigins
Definition Trace.hpp:135