Management of Code Dependencies¶
The Management of dependencies from compiled methods to classes, methods and other entities where such dependencies may change and result in some action (e.g. deoptimization) being applied to a compiled method.
A dependency is a relationship between a
`TargetMethod <./Glossary#target-method>`__, that is, the result of a compilation, and an assumption that was made by the compiler during the compilation.
The assumption may be any invariant that can be checked for validity at a future time.
Assumptions are specified by subclasses of
Instances of such classes typically contain references to VM objects that, for example, represent methods, i.e.,
Note that assumptions at this level are generally specified using compiler and VM independent types, and are defined in a compiler and VM independent project (package).
However, there is nothing that prevents a VM specific assumption being defined using VM specific types.
Since an assumption has to be validated any time the global state of the VM changes, for example, a new class is loaded, it must persist as long as the associated
To minimize the amount of storage space occupied by assumptions, and to simplify analysis in a concrete VM, validated assumptions are converted to dependencies, which use a densely encoded form of the concrete VM types using small integers, such as
All assumptions have an associated context class which identifies the class that the assumption affects.
For example, the ConcreteSubtype assumption specifies that a class
T has a single unique subtype
In this case,
T is defined to be the context class.
The possible set of assumptions and associated dependencies is open-ended.
In order to provide for easy extensibility while keeping the core of the system independent, the concept of a
DependencyProcessor is introduced.
DependencyProcessor is responsible for the following:
- the validation of the associated assumption.
- the encoding of the assumption into an efficient packed form
- the processing of the packed form, converting back to an object form for ease of analysis
- supporting the application of a dependency visitor for analysis
- providing a string based representation of the dependency for tracing
A visitor pattern is used to support the analysis of a
Recall that each such instance relates to a single
TargetMethod, may contain dependencies related to several context classes and each of these may contain dependencies corresponding to several dependency processors.
Since the set of
DependencyProcessors is open ended, and a visitor may want to visit the data corresponding to several dependency processors in one visit, implementation class inheritance cannot be used to create a specific visitor.
Instead, a two-level type structure is used, with interfaces defined in the specific
DependencyProcessor class that declare the statically typed methods that result from decoding the packed form of the dependency.
Note that these typically correspond closely to the original
CiAssumptions.Assumption but with compiler/VM independent types replaced with Maxine specific types.
RiResolvedType replaced with
Dependencies.DependencyVisitor handles the aspects of the iteration that are independent of the dependency processors.
Dependencies.DependencyVisitor for more details.
The data for each dependency processor is visited by invoking
Dependencies.DependencyVisitor.visit for each individual dependency.
This method is generic since it cannot know anything about the types of the data associated with the dependency.
The default implementation handles this by calling
DependencyProcessor.match which returns
dependencyVisitor if the visitor implements the
DependencyProcessorVisitor interface defined by the processor that specifies the types of the data in the dependency, or
null if not.
It then invokes
DependencyProcessor.visit with this value, which invokes the typed method in the interface if the value is non-null, and steps the index to the next dependency.
DependencyProcessor.visit this way allows a different
DependencyProcessorVisitor to be called by an overriding implementation of
For example, a visitor that cannot know all the dependency processors in the system, yet wants to invoke the
Defining a new Dependency Processor¶
The first step is to define a new subclass of
If, as is typical, the dependency is used within the optimizing compiler, then this subclass should be defined by adding it to
Next define a subclass of
DependencyProcessor that will handle this assumption in Maxine, and place it in the
Define a nested interface that extends of
DependencyProcessorVisitor and defines a method with the same arguments as the method in the
To support generic tracing of dependencies you should also define a subclass of
DependencyProcessor.ToStringDependencyProcessorVisitor that implements your interface method(s) and appends appropriate tracing data to the
StringBuilder variable in
Define a static final instance of the
DependencyProcessor subclass, which will cause it to be registered with
DependenciesManager during boot image generation.
Finally, implement the remaining abstract methods:
The first two have trivial implementations.
The visit method must step over the specific dependency data and, if the
dependencyProcessorVisitor is not null, invoke the associated method, with the encoded data transformed into the appropriate argument types.
Evidently, if the visitor is null, processing related to transforming the encoded data should be avoided.
Automatically generated from