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

Backwards reaching definitions #726

Merged
merged 21 commits into from
Sep 21, 2024

Conversation

xeren
Copy link
Collaborator

@xeren xeren commented Sep 4, 2024

This PR decouples an interface for Dependency and UseDefAnalysis, and adds an alternative implementation for it, BackwardsReachingDefinitionsAnalysis (BRDA). It takes into account, that readers on uninitialized registers are unlikely, while final writers are frequent, although rarely useful. This means, that the propagated data is smaller in BRDA, than in the existing forward-directed implementations.
The more noteworthy feature is, that I wrote it to support loops. This means that it, as well as the dependent alias analysis could potentially be done before unrolling.

@xeren xeren changed the base branch from master to development September 4, 2024 11:51
@ThomasHaas
Copy link
Collaborator

UseDefAnalysis also supported loops. But I guess your new analysis might be more efficient?

@xeren
Copy link
Collaborator Author

xeren commented Sep 4, 2024

Yes, it should have the capabilities of both implementations and be more efficient.

@ThomasHaas
Copy link
Collaborator

Also, why do you use BranchEquivalence rather than ExecutionAnalysis? I think the latter is more correct because it also considers that instructions may fail to execute.

@ThomasHaas
Copy link
Collaborator

@hernanponcedeleon Do you remember on which benchmark UseDefAnalysis ran out of memory? I think you had some huge benchmarks where this was a problem. If so, it would be interesting to test this new analysis for efficiency.

@xeren
Copy link
Collaborator Author

xeren commented Sep 4, 2024

Also, why do you use BranchEquivalence rather than ExecutionAnalysis? I think the latter is more correct because it also considers that instructions may fail to execute.

Because the cfImpliesExec condition is only needed in the over approximation (the 'may' relation). BranchEquivalence on the other hand, more precisely the areMutuallyExclusive relation, is only needed in the under approximation (the 'conditionally-must' relation). Since the later is optional, BE is not mandatory.

@xeren
Copy link
Collaborator Author

xeren commented Sep 4, 2024

By the way, using the analysis has the side effect, that final register encodings are omitted, if the register is not used by the spec. This could be a small issue fixed.

@ThomasHaas
Copy link
Collaborator

Also, why do you use BranchEquivalence rather than ExecutionAnalysis? I think the latter is more correct because it also considers that instructions may fail to execute.

Because the cfImpliesExec condition is only needed in the over approximation (the 'may' relation). BranchEquivalence on the other hand, more precisely the areMutuallyExclusive relation, is only needed in the under approximation (the 'conditionally-must' relation). Since the later is optional, BE is not mandatory.

I'm not sure I understand. It seems you could just use ExecutionAnalysis.areMutuallyExclusive in your code to both simplify it and be more accurate (in theory). Conceptually, when analysing dataflow you care about executed instructions rather than instructions in the control-flow.
It likely won't make a practical difference in your use-case, because the ExecutionAnalysis is based only on control-flow but I don't see why your analysis should be aware of that and "optimize" for it.

@hernanponcedeleon
Copy link
Owner

@hernanponcedeleon Do you remember on which benchmark UseDefAnalysis ran out of memory? I think you had some huge benchmarks where this was a problem. If so, it would be interesting to test this new analysis for efficiency.

I don't really remember which benchmark that was. But I can try this in some of our larger ones.

@hernanponcedeleon
Copy link
Owner

This means, that the propagated data is smaller in BRDA, than in the existing forward-directed implementations.

Which part of our pipelines are (positively) affected by this? E.g., does the alias analysis potentially becomes more precise? Do we get smaller mayset in idd?

@ThomasHaas
Copy link
Collaborator

I think this statement is not about precision but just about memory consumption. The forward-directed implementation kept information about (last) register writes even if those registers were never going to get used again.

@xeren
Copy link
Collaborator Author

xeren commented Sep 9, 2024

Which part of our pipelines are (positively) affected by this? E.g., does the alias analysis potentially becomes more precise? Do we get smaller mayset in idd?

Compared to UseDefAnalysis and Dependency, the results of BRDA for any RegReader should be exactly the same. So, any dependent analysis should be relatively unaffected, up until the encoder. idd stays the same. With propagated data, I just meant the internal updates of BRDA and Dependency. At best, there could be side effects on BRDA's querying performance, based on the data layout I chose, i.e. the time it needs to fetch a list of writers.

Since UseDefAnalysis and Dependency are unused now, I probably should add the following options, before merging.

  • program.processing.loopBounds.useDefAnalysis = {FORWARD, BACKWARD}
  • program.analysis.reachingDefinitions = {FORWARD, BACKWARD}

@ThomasHaas
Copy link
Collaborator

If the analysis work fine, I don't think we need to keep around the unused options.

@hernanponcedeleon
Copy link
Owner

Keeping two implementations of the analysis might be good: if we encounter any example where one is too slow, we can try another (similar to what happened with the alias analysis and wsq.c).

However, keeping options for each single pass making use of the analysis seems too fine-grained without much clear benefit. I would rather always use forward or always us backwards.

Comment on lines 26 to 43
/**
* Collects all direct usage relationships between {@link RegWriter} and {@link RegReader}.
* <p>
* In contrast to a usual Reaching-Definitions-Analysis,
* this implementation analyzes the program from back to front,
* assigning each program point the set of readers,
* who may still require a register initialization.
* This means that it does not collect definitions for unused registers.
* Especially, it does not collect last writers for all registers.
* <p>
* This analysis is control-flow-sensitive;
* that is, {@link Label} splits the information among the jumps to it
* and {@link CondJump} merges the information.
* <p>
* This analysis supports loops;
* that is, backward jumps cause re-evaluation of the loop body until convergence.
* This results in a squared worst-case time complexity in terms of events being processed.
*/

Choose a reason for hiding this comment

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

Can you somehow format the doc? It hurst my eyes that widths are so uneven XD

public Writers getWriters(RegReader reader) {
Preconditions.checkNotNull(reader, "reader is null");
final ReaderInfo result = readerMap.get(reader);
Preconditions.checkArgument(result != null, "reader %s has not been analyzed.", reader);

Choose a reason for hiding this comment

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

probably it should be checkState

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Valid, if reader.getFunction() had been analyzed by this. I considered more likely, that a user would try to query events of another function or program.

}

/**
* Analyzes an entire set of threads.

Choose a reason for hiding this comment

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

"an entire set of threads." -> "the entire program (after thread creation)" ... you are not passing any threads as parameter

Comment on lines 213 to 228
for (Event event : function.getEvents()) {
if (event instanceof RegWriter writer) {
writerMap.put(writer, new Readers());
}
}
writerMap.put(null, new Readers());
for (Event event : function.getEvents()) {
if (event instanceof RegReader reader) {
final Set<Register> usedRegisters = new HashSet<>();
for (Register.Read read : reader.getRegisterReads()) {
usedRegisters.add(read.register());
}
readerMap.put(reader, new ReaderInfo(usedRegisters));
}
}
readerMap.put(null, new ReaderInfo(finalRegisters));

Choose a reason for hiding this comment

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

Why not process everything in the same loop iteration? I don't see any dependency from the second loop tot eh first one.

Also, what are these X.put(null, ...) representing?

Copy link
Collaborator

Choose a reason for hiding this comment

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

The null entry is likely for reads from uninitialized registers.
I agree about the loop: either you merge them or you keep them and iterate over getEvents(RegWriter.class) resp. getEvents(RegReader.class) to skip the instanceof checks.

Choose a reason for hiding this comment

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

The null entry is likely for reads from uninitialized registers.

Whatever this is used for, it should be documented, I don't want someone reading the code to have to guess

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep, since there are no dependencies, both versions are valid. I consider my version better in terms of cache optimization, but not by much.

In writerMap, null stands for the initial writer(s). In readerMap, it denotes the final reader(s). Both cases don't have actual Event instances. In this analysis, they behave as if they had. For readability, I added named null constants.

analysis.run(function, finalRegisters);
}
analysis.postProcess();
if (exec != null && program.isUnrolled()) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The analysis can be run during processing where ExecutionAnalysis is not available, but it can also be run afterwards. In the former case it should subsume UseDefAnalysis and in the latter case Dependency.

Comment on lines 213 to 228
for (Event event : function.getEvents()) {
if (event instanceof RegWriter writer) {
writerMap.put(writer, new Readers());
}
}
writerMap.put(null, new Readers());
for (Event event : function.getEvents()) {
if (event instanceof RegReader reader) {
final Set<Register> usedRegisters = new HashSet<>();
for (Register.Read read : reader.getRegisterReads()) {
usedRegisters.add(read.register());
}
readerMap.put(reader, new ReaderInfo(usedRegisters));
}
}
readerMap.put(null, new ReaderInfo(finalRegisters));
Copy link
Collaborator

Choose a reason for hiding this comment

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

The null entry is likely for reads from uninitialized registers.
I agree about the loop: either you merge them or you keep them and iterate over getEvents(RegWriter.class) resp. getEvents(RegReader.class) to skip the instanceof checks.

@ThomasHaas
Copy link
Collaborator

Anything that needs to be done here? I have not checked the algorithmic details, but given that all tests pass it is likely working as intended.

@hernanponcedeleon
Copy link
Owner

Unless I missed something, not all comments were fixed. From the top of my head:

  • we still have fine grained options that I don't like
  • some of the preconditions / checks need to be streamlined

…ackwards-reaching-definitions

# Conflicts:
#	dartagnan/src/main/java/com/dat3m/dartagnan/Dartagnan.java
#	dartagnan/src/main/java/com/dat3m/dartagnan/configuration/OptionNames.java
#	dartagnan/src/main/java/com/dat3m/dartagnan/encoding/ProgramEncoder.java
#	dartagnan/src/main/java/com/dat3m/dartagnan/utils/options/BaseOptions.java
#	dartagnan/src/main/java/com/dat3m/dartagnan/verification/solving/ModelChecker.java
#	dartagnan/src/main/java/com/dat3m/dartagnan/verification/solving/RefinementSolver.java
#	dartagnan/src/test/java/com/dat3m/dartagnan/miscellaneous/AnalysisTest.java
#	dartagnan/src/test/java/com/dat3m/dartagnan/utils/rules/Providers.java
Remove NaiveLoopBoundAnnotation.newInstance()
Add NaiveLoopBoundAnnotation.fromConfig(Configuration)
Add ReachingDefinitionsAnalysis.configure(Configuration)
Set BackwardsReachingDefinitionsAnalysis package-private
@hernanponcedeleon
Copy link
Owner

I think all my comments have been solved. Unless @ThomasHaas has some more comments, I'll merge

Copy link
Collaborator

@ThomasHaas ThomasHaas left a comment

Choose a reason for hiding this comment

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

LGTM

@hernanponcedeleon hernanponcedeleon merged commit 73ba96c into development Sep 21, 2024
1 check passed
@hernanponcedeleon hernanponcedeleon deleted the backwards-reaching-definitions branch September 21, 2024 06:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants