Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an AccessPath abstraction and formalize memory access #34126

Merged
merged 20 commits into from
Oct 19, 2020

Conversation

atrick
Copy link
Contributor

@atrick atrick commented Sep 30, 2020

Things that have come up recently but are somewhat blocked on this:

- Moving AccessMarkerElimination down in the pipeline
- SemanticARCOpts correctness and improvements
- AliasAnalysis improvements
- LICM performance regressions
- RLE/DSE improvements

Begin to formalize the model for valid memory access in SIL. Ignoring
ownership, every access is a def-use chain in three parts:

object root -> formal access base -> memory operation address

AccessPath abstracts over this path and standardizes the identity of a
memory access throughout the optimizer. This abstraction is the basis
for a new AccessPathVerification.

With that verification, we now have all the properties we need for the
type of analysis requires for exclusivity enforcement, but now
generalized for any memory analysis. This is suitable for an extremely
lightweight analysis with no side data structures. We currently have a
massive amount of ad-hoc memory analysis throughout SIL, which is
incredibly unmaintainable, bug-prone, and not performance-robust. We
can begin taking advantage of this verifably complete model to solve
that problem.

The properties this gives us are:

Access analysis must be complete over memory operations: every memory
operation needs a recognizable valid access. An access can be
unidentified only to the extent that it is rooted in some non-address
type and we can prove that it is at least *not* part of an access to a
nominal class or global property. Pointer provenance is also required
for future IRGen-level bitfield optimizations.

Access analysis must be complete over address users: for an identified
object root all memory accesses including subobjects must be
discoverable.

Access analysis must be symmetric: use-def and def-use analysis must
be consistent.

AccessPath is merely a wrapper around the existing accessed-storage
utilities and IndexTrieNode. Existing passes already very succesfully
use this approach, but in an ad-hoc way. With a general utility we
can:

- update passes to use this approach to identify memory access,
  reducing the space and time complexity of those algorithms.

- implement an inexpensive on-the-fly, debug mode address lifetime analysis

- implement a lightweight debug mode alias analysis

- ultimately improve the power, efficiency, and maintainability of
  full alias analysis

- make our type-based alias analysis sensistive to the access path

@atrick
Copy link
Contributor Author

atrick commented Sep 30, 2020

Discussion in #33121

@atrick
Copy link
Contributor Author

atrick commented Sep 30, 2020

@swift-ci test

@atrick
Copy link
Contributor Author

atrick commented Sep 30, 2020

@swift-ci test source compatibility

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 0fd6be8

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 0fd6be8

Copy link
Contributor

@eeckstein eeckstein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation is much better now, thanks!
I still have a few comments.
And I still have to review the implementation.

docs/SILProgrammersManual.md Outdated Show resolved Hide resolved
docs/SILProgrammersManual.md Outdated Show resolved Hide resolved
docs/SILProgrammersManual.md Outdated Show resolved Hide resolved
docs/SILProgrammersManual.md Outdated Show resolved Hide resolved
docs/SILProgrammersManual.md Outdated Show resolved Hide resolved
docs/SILProgrammersManual.md Outdated Show resolved Hide resolved
docs/SILProgrammersManual.md Outdated Show resolved Hide resolved
docs/SILProgrammersManual.md Outdated Show resolved Hide resolved
docs/SILProgrammersManual.md Show resolved Hide resolved
docs/SILProgrammersManual.md Outdated Show resolved Hide resolved
@atrick
Copy link
Contributor Author

atrick commented Sep 30, 2020

@swift-ci test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 810a62047788db057dcd7770fa00c383595ce480

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 810a62047788db057dcd7770fa00c383595ce480

@atrick
Copy link
Contributor Author

atrick commented Oct 2, 2020

@swift-ci test

@swift-ci
Copy link
Contributor

swift-ci commented Oct 2, 2020

Build failed
Swift Test Linux Platform
Git Sha - 810a62047788db057dcd7770fa00c383595ce480

case AccessedStorage::Unidentified:
return getValue(); // Can be invalid for Unidentified storage.
case AccessedStorage::Global:
return SILValue();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is a global not a valid root?

@@ -427,18 +464,17 @@ class AccessedStorage {

/// If this is a uniquely identified formal access, then it cannot
/// alias with any other uniquely identified access to different storage.
///
/// This determines whether access markers may conflict, so it cannot assume
/// that exclusivity is enforced.
bool isUniquelyIdentified() const {
switch (getKind()) {
case Box:
case Stack:
case Global:
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here: why is a global uniquely identified, while a class is not?


// Special-case this indirect enum pattern:
// unchecked_take_enum_data_addr -> load -> project_box
// (the individual load and project_box are not access projections)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure you want to model indirect enum cases as address projections?
It might work, but can you prove that this does not introduce inconsistencies with the model?
It would imply to deal with potentially infinitely large projection paths.

This feels like you are adding an abstraction layer for exactly this pattern. Without this we would have the simple model of that everything which is a reference is a root object.

@@ -136,11 +136,18 @@ static inline bool isCastProjectionKind(ProjectionKind Kind) {
/// that immediately contains it.
///
/// This lightweight utility maps a SIL address projection to an index.
///
/// project_box does not have a projection index. At the SIL level, the box
/// storage is considered part of the same object as the. The box projection is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: "... as the."

// only used by visitors that process access projections; once the accessed
// address is reached, they are no longer relevant.
template <typename UseDefVisitor>
class AccessPhiVisitor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a big fan of these visitors. It might be satisfying from a code-aesthetic point of view, but it adds complexity which is hard to understand. Each visitor has its own state and the algorithm, how all this works, is obfuscated.
I would prefer an old-fashioned, more functional, style for implementing the use-def analysis.


// Find the origin of an access while skipping projections and casts and
// handling phis.
template <typename Impl>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and templates add another level of complexity

// AccessPath
//===----------------------------------------------------------------------===//

bool AccessPath::contains(AccessPath subPath) const {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In contrast: "contains" and "mayOverlap" are beautiful - simple and easy to understand!

// If subpaths are disjoint, they do not overlap regardless of offset.
if (!pathNode.node->isPrefixOf(otherPath.pathNode.node)
&& !otherPath.pathNode.node->isPrefixOf(pathNode.node)) {
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be "return false"?

if (storage.getKind() == AccessedStorage::Class) {
pathIndices.push_back(AccessPath::Index::forSubObjectProjection(
storage.getPropertyIndex()));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

} else if...

@atrick
Copy link
Contributor Author

atrick commented Oct 15, 2020

@gottesmm In order to stage in new verification, load-borrow immutability gets disabled in this PR but immediately reenabled here: https://github.com/atrick/swift/commits/rewrite-loadborrowchecker

@atrick
Copy link
Contributor Author

atrick commented Oct 15, 2020

@swift-ci test

@atrick
Copy link
Contributor Author

atrick commented Oct 15, 2020

@swift-ci test source compatibility

@atrick
Copy link
Contributor Author

atrick commented Oct 15, 2020

@swift-ci benchmark

@swift-ci
Copy link
Contributor

Performance: -O

Regression OLD NEW DELTA RATIO
DropFirstArray 13 14 +7.7% 0.93x (?)

Code size: -O

Performance: -Osize

Improvement OLD NEW DELTA RATIO
DropFirstCountableRange 15 14 -6.7% 1.07x (?)

Code size: -Osize

Performance: -Onone

Regression OLD NEW DELTA RATIO
DataReplaceLargeBuffer 12 13 +8.3% 0.92x (?)

Code size: -swiftlibs

How to read the data The tables contain differences in performance which are larger than 8% and differences in code size which are larger than 1%.

If you see any unexpected regressions, you should consider fixing the
regressions before you merge the PR.

Noise: Sometimes the performance results (not code size!) contain false
alarms. Unexpected regressions which are marked with '(?)' are probably noise.
If you see regressions which you cannot explain you can try to run the
benchmarks again. If regressions still show up, please consult with the
performance team (@eeckstein).

Hardware Overview
  Model Name: Mac mini
  Model Identifier: Macmini8,1
  Processor Name: 6-Core Intel Core i7
  Processor Speed: 3.2 GHz
  Number of Processors: 1
  Total Number of Cores: 6
  L2 Cache (per Core): 256 KB
  L3 Cache: 12 MB
  Memory: 64 GB

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - eb5f956be9e0002d2811098350ccbf941a31d4fa

@atrick
Copy link
Contributor Author

atrick commented Oct 15, 2020

@swift-ci test

@atrick
Copy link
Contributor Author

atrick commented Oct 15, 2020

@swift-ci test source compatibility

@gottesmm
Copy link
Contributor

@atrick that's a huge patch. If you are going to disable it, now that if anything creeps in, you need to fix.

@atrick
Copy link
Contributor Author

atrick commented Oct 15, 2020

@gottesmm I've actually been waiting to land this PR until I know all the subsequent functionality, including the load-borrow-checker rewrite will pass all testing and SCK. So the only reason to disable load borrow checking here is to break up the functionality and allow the bots to churn through one piece at a time.

@gottesmm
Copy link
Contributor

Sure. I am cool with it. I am just saying if something sneaks in... its on you to fix = p.

...and avoid reallocation.

This is immediately necessary for LICM, in addition to its current
uses. I suspect this could be used by many passes that work with
addresses. RLE/DSE should absolutely migrate to it.
Change ProjectionIndex for ref_tail_addr to std::numeric_limits<int>::max();
This is necessary to disambiguate the tail elements from
ref_element_addr field zero.
atrick added 17 commits October 16, 2020 15:00
To clarify and unify logic, improve precision, and behave consistently
with other code that does the same thing.
Things that have come up recently but are somewhat blocked on this:

- Moving AccessMarkerElimination down in the pipeline
- SemanticARCOpts correctness and improvements
- AliasAnalysis improvements
- LICM performance regressions
- RLE/DSE improvements

Begin to formalize the model for valid memory access in SIL. Ignoring
ownership, every access is a def-use chain in three parts:

object root -> formal access base -> memory operation address

AccessPath abstracts over this path and standardizes the identity of a
memory access throughout the optimizer. This abstraction is the basis
for a new AccessPathVerification.

With that verification, we now have all the properties we need for the
type of analysis requires for exclusivity enforcement, but now
generalized for any memory analysis. This is suitable for an extremely
lightweight analysis with no side data structures. We currently have a
massive amount of ad-hoc memory analysis throughout SIL, which is
incredibly unmaintainable, bug-prone, and not performance-robust. We
can begin taking advantage of this verifably complete model to solve
that problem.

The properties this gives us are:

Access analysis must be complete over memory operations: every memory
operation needs a recognizable valid access. An access can be
unidentified only to the extent that it is rooted in some non-address
type and we can prove that it is at least *not* part of an access to a
nominal class or global property. Pointer provenance is also required
for future IRGen-level bitfield optimizations.

Access analysis must be complete over address users: for an identified
object root all memory accesses including subobjects must be
discoverable.

Access analysis must be symmetric: use-def and def-use analysis must
be consistent.

AccessPath is merely a wrapper around the existing accessed-storage
utilities and IndexTrieNode. Existing passes already very succesfully
use this approach, but in an ad-hoc way. With a general utility we
can:

- update passes to use this approach to identify memory access,
  reducing the space and time complexity of those algorithms.

- implement an inexpensive on-the-fly, debug mode address lifetime analysis

- implement a lightweight debug mode alias analysis

- ultimately improve the power, efficiency, and maintainability of
  full alias analysis

- make our type-based alias analysis sensistive to the access path
Add a flag: enable-accessed-storage-dump-uses
Add AccesssedStorage::compute and computeInScope to mirror AccessPath.

Allow recovering the begin_access for Nested storage.

Adds AccessedStorage.visitRoots().
Compute 'isLet' from the VarDecl that is available when constructing
AccessedStorage so we don't need to recover the VarDecl for the base
later.

This generally makes more sense and is more efficient, but it will be
necessary when we look past class casts when finding the reference root.
A rewrite is ready and will be merged ASAP.
@atrick
Copy link
Contributor Author

atrick commented Oct 16, 2020

@swift-ci smoke test

@atrick atrick merged commit fd8f723 into swiftlang:main Oct 19, 2020
@atrick atrick deleted the add-accesspath branch November 9, 2020 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants