Skip to content

Commit

Permalink
[docs] Add details to MemorySSA docs.
Browse files Browse the repository at this point in the history
Add more details to the docs regarding optimized accesses for Uses and Defs.
Include incoming changes from https://reviews.llvm.org/D121381.

Differential Revision: https://reviews.llvm.org/D121740
  • Loading branch information
alinas committed Mar 17, 2022
1 parent 7062094 commit 187a5f2
Showing 1 changed file with 78 additions and 25 deletions.
103 changes: 78 additions & 25 deletions llvm/docs/MemorySSA.rst
Original file line number Diff line number Diff line change
Expand Up @@ -256,28 +256,90 @@ they ultimately form a linked list of every clobber that dominates the
``MemoryDef`` or ``MemoryPhi`` of said ``MemoryDef``.


Build-time use optimization
---------------------------

``MemorySSA`` will optimize some ``MemoryAccess``\ es at build-time.
Specifically, we optimize the operand of every ``MemoryUse`` to point to the
Use and Def optimization
------------------------

``MemoryUse``\ s keep a single operand, which is their defining or optimized
access.
Traditionally ``MemorySSA`` optimized ``MemoryUse``\ s at build-time, up to a
given threshold.
Specifically, the operand of every ``MemoryUse`` was optimized to point to the
actual clobber of said ``MemoryUse``. This can be seen in the above example; the
second ``MemoryUse`` in ``if.end`` has an operand of ``1``, which is a
``MemoryDef`` from the entry block. This is done to make walking,
value numbering, etc, faster and easier.
As of `this revision <https://reviews.llvm.org/D121381>`_, the default was
changed to not optimize uses at build time, in order to provide the option to
reduce compile-time if the walking is not necessary in a pass. Most users call
the new API ``ensureOptimizedUses()`` to keep the previous behavior and do a
one-time optimization of ``MemoryUse``\ s, if this was not done before.
New pass users are recommended to call ``ensureOptimizedUses()``.

Initially it was not possible to optimize ``MemoryDef``\ s in the same way, as we
restricted ``MemorySSA`` to one operand per access.
This was changed and ``MemoryDef``\ s now keep two operands.
The first one, the defining access, is
always the previous ``MemoryDef`` or ``MemoryPhi`` in the same basic block, or
the last one in a dominating predecessor if the current block doesn't have any
other accesses writing to memory. This is needed for walking Def chains.
The second operand is the optimized access, if there was a previous call on the
walker's ``getClobberingMemoryAccess(MA)``. This API will cache information
as part of ``MA``.
Optimizing all ``MemoryDef``\ s has quadratic time complexity and is not done
by default.

A walk of the uses for any MemoryDef can find the accesses that were optimized
to it.
A code snippet for such a walk looks like this:

.. code-block:: c++
MemoryDef *Def; // find who's optimized or defining for this MemoryDef
for (auto& U : Def->uses()) {
MemoryAccess *MA = cast<MemoryAccess>(Use.getUser());
if (auto *DefUser = cast_of_null<MemoryDef>MA)
if (DefUser->isOptimized() && DefUser->getOptimized() == Def) {
// User who is optimized to Def
} else {
// User who's defining access is Def; optimized to something else or not optimized.
}
}
It is not possible to optimize ``MemoryDef`` in the same way, as we
restrict ``MemorySSA`` to one memory variable and, thus, one Phi node
per block.
When ``MemoryUse``\ s are optimized, for a given store, you can find all loads
clobbered by that store by walking the immediate and transitive uses of
the store.

.. code-block:: c++
checkUses(MemoryAccess *Def) { // Def can be a MemoryDef or a MemoryPhi.
for (auto& U : Def->uses()) {
MemoryAccess *MA = cast<MemoryAccess>(Use.getUser());
if (auto *MU = cast_of_null<MemoryUse>MA) {
// Process MemoryUse as needed.
}
else {
// Process MemoryDef or MemoryPhi as needed.
// As a user can come up twice, as an optimized access and defining
// access, keep a visited list.

// Check transitive uses as needed
checkUses (MA); // use a worklist for an iterative algorithm
}
}
}

An example of similar traversals can be found in the DeadStoreElimination pass.

Invalidation and updating
-------------------------

Because ``MemorySSA`` keeps track of LLVM IR, it needs to be updated whenever
the IR is updated. "Update", in this case, includes the addition, deletion, and
motion of ``Instructions``. The update API is being made on an as-needed basis.
If you'd like examples, ``GVNHoist`` is a user of ``MemorySSA``\ s update API.
If you'd like examples, ``GVNHoist`` and ``LICM`` are users of ``MemorySSA``\ s
update API.
Note that adding new ``MemoryDef``\ s (by calling ``insertDef``) can be a
time-consuming update, if the new access triggers many ``MemoryPhi`` insertions and
renaming (optimization invalidation) of many ``MemoryAccesses``\ es.


Phi placement
Expand Down Expand Up @@ -403,34 +465,25 @@ Precision in practice
^^^^^^^^^^^^^^^^^^^^^

In practice, there are implementation details in LLVM that also affect the
results' precision provided by MemorySSA. For example, AliasAnalysis has various
caps, or restrictions on looking through phis which can affect what MemorySSA
results' precision provided by ``MemorySSA``. For example, AliasAnalysis has various
caps, or restrictions on looking through phis which can affect what ``MemorySSA``
can infer. Changes made by different passes may make MemorySSA either "overly
optimized" (it can provide a more acccurate result than if it were recomputed
from scratch), or "under optimized" (it could infer more if it were recomputed).
This can lead to challenges to reproduced results in isolation with a single pass
when the result relies on the state aquired by MemorySSA due to being updated by
when the result relies on the state aquired by ``MemorySSA`` due to being updated by
multiple subsequent passes.
Passes that use and update MemorySSA should do so through the APIs provided by the
MemorySSAUpdater, or through calls on the Walker.
Direct optimizations to MemorySSA are not permitted.
Passes that use and update ``MemorySSA`` should do so through the APIs provided by the
``MemorySSAUpdater``, or through calls on the Walker.
Direct optimizations to ``MemorySSA`` are not permitted.
There is currently a single, narrowly scoped exception where DSE (DeadStoreElimination)
updates an optimized access of a store, after a traversal that guarantees the
optimization is correct. This is solely allowed due to the traversals and inferences
being beyond what MemorySSA does and them being "free" (i.e. DSE does them anyway).
being beyond what ``MemorySSA`` does and them being "free" (i.e. DSE does them anyway).
This exception is set under a flag ("-dse-optimize-memoryssa") and can be disabled to
help reproduce optimizations in isolation.


Use Optimization
^^^^^^^^^^^^^^^^

Unlike other partitioned forms, LLVM's ``MemorySSA`` does make one
useful guarantee - all loads are optimized to point at the thing that
actually clobbers them. This gives some nice properties. For example,
for a given store, you can find all loads actually clobbered by that
store by walking the immediate uses of the store.

LLVM Developers Meeting presentations
-------------------------------------

Expand Down

0 comments on commit 187a5f2

Please sign in to comment.