## Problem

When C2 parses a variable, it tries to be smart about the type of that 
variable. If a method parameter declares that it is of type `A`, and `A` is an 
abstract class that only has a single concrete subclass `B`, then C2 eagerly 
assumes that the parameter must be of type `B`. This property, in the presence 
of concurrent class loading/unloading, does not always hold, as the VM can 
always load another concrete subclass `C` of `A` that is unrelated to `B`. To 
avoid incorrect execution arised from this issue, C2 relies on `dependencies`, 
which encodes various kinds of assumptions the compiler made. Among those, we 
have an assumption called `unique_concrete_subclass`, which will be invalidated 
when `C` is loaded, triggering the deoptimization and invalidation of the 
aforementioned compiled method.

However, this mechanism is both inadequate and not timely. The main issue is 
that the concurrent load of `C` may not only happen after the compilation has 
finished, but also during the compilation process. This leads to CHA 
computation giving contradictory results when called at different time in the 
compilation process. There are 2 examples which were reported:

1.

    if (b) {
        A a1 = ...;
    } else {
        C a2 = ...;
    }
    A a = Phi(a1, a2);

If `A a = Phi(a1, a2)` is parsed before `C a2 = ...`, and `C` has not been 
loaded then, the compiler will assume that `a` must be of type `B`. This leads 
to a `Phi` of type `B` with one of its inputs being a node of type `C` which is 
unrelated. This graph shape is nonsense, and all kinds of issues can appear 
from various optimization passes which do not expect such a conflicting graph.

2.

    A a = call();

If when `a` is parsed, `C` has not been loaded, the compiler will assume `a` is 
of type `B`. However, if `call` is late-inlined, and it turns out that the 
returned value is of type `C`. All uses of `a` will suddenly have their input 
changed from a `B` to a `C`, which may also lead to unexpected failures.

## Overview of this PR

This PR tries to detect CHA inconsistencies, and triggers a compilation failure 
when such a paradox happens. The core invariant that needs holding is that, 
given type `X` and `Y`, if `X` is a subtype of `Y`, then the result of 
narrowing `X` should also be a subtype of the result of narrowing `Y`. This 
invariant does not need to hold in the VM, it only needs to hold in the local 
`ciEnv`, which is the local view of the compiler with respect to the class 
hierarchy.

In some cases, we can avoid such a paradox from arising, such as when CHA would 
load a class that would lead to a paradoxical hierarchy view. In those cases, 
we can do a pre-check and avoid loading that class into the `ciEnv`, so that 
the compiler is not exposed to that paradox. In other cases, loading the class 
into the `ciEnv` is inevitable, as it appears in the class file. In those 
cases, the `ciEnv` instance will record the paradox, and C2 will abort 
compilation accordingly.

## Design considerations

#### Avoid failing the compilation

An alternative approach I have pursued is to ensure that a paradoxical 
hierarchy view is always avoided. This has a couple of drawbacks:

- Since parsing can lead to several classes that must be loaded. We either need 
to change it so that parsing will stop if it encounters a class that leads to a 
paradox, or move all CHA inference to after all parsing, or even after escape 
analysis. Both approaches are complicated, and it is hard to ensure that a 
class is never loaded so that a paradox is seen because there are many places 
which load them. One of the unexpected places that loads `ciInstanceKlass` is 
`ciMethod::method_data` which may load classes corresponding to the profiled 
receiver.
- There are a lot of optimizations that benefit from a class having a sharp 
type. In principle, they should also be applicable during IGVN, but doing so 
would require non-trivial effort.

#### Paradoxical hierarchy immediately fails the `ciEnv`

Currently, when a paradoxical hierarchy is found, we mark the corresponding 
flag in `ciEnv` without failing it. This is because only C2 cares about the 
consistency of CHA computation, `ciTypeFlow` can work just fine. In addition, 
`ciTypeFlow` does not expect spontaneous failures when calling 
`ciEnv::get_instance_klass`, so it would be cleaner to make only C2 check for 
such inconsistencies.

## Testing

I have not managed to come up with a dedicated test case for this fix, because 
there are only a limited number of cases where the inconsistency leads to 
incorrectly compiled code and the compiled code is not discarded by 
dependencies. As a result, I reverted the related changes that were temporary 
fixes for this issue and observed no failure from the tests added by those 
fixes.

- [ ] tier1-4,hs-comp-stress,hs-aot-stress

## Some adjustments to existing code and tests

- `ciKlass::is_leaf_type` is unused.
- `Dependency::check_unique_implementor` seems dubious, there is no guarantee 
that the assumption is not violated after the call to 
`ciInstanceKlass::unique_implementor` and before the call to 
`Dependencies::assert_unique_implementor`.
- `TestMergeStore` assumes that `Cloneable` is always ordered before 
`Serializeable` when printing an array klass, this is not true, the added 
interfaces also do not add any more verification rigor, so I removed them.
- In `TestNarrowPhi`, I replaced the combination of calling 
`WhiteBox::enqueueMethodForCompilation` and waiting for 
`WhiteBox::isMethodQueuedForCompilation` with simply waiting for 
`WhiteBox::enqueueMethodForCompilation` under `-Xbatch`, which achieves the 
same effect but is much better.
- In `Continuation/Fuzz.java`, the dance of calling 
`enqueueMethodForCompilation` and `isMethodQueuedForCompilation` does not work 
when the compilation may fail. Especially for complicated methods such as those 
in the tests. I also replace it with simply calling 
`enqueueMethodForCompilation` under `-Xbatch`.

Please take a look and leave your reviews, thanks a lot.

---------
- [x] I confirm that I make this contribution in accordance with the [OpenJDK 
Interim AI Policy](https://openjdk.org/legal/ai).

-------------

Commit messages:
 - Fix Coninuation/Fuzz, revert PhaseCCP temporary fix
 - Fix keep_alive mechanism, remove old Phi input workaround, fix test 
failures, change paradoxical_hierarchy not to fail ciEnv
 - has_subklass may be called from VM
 - Fix keep_alive
 - Ensure the hierarchy view is consistent

Changes: https://git.openjdk.org/jdk/pull/33753/files
  Webrev: https://webrevs.openjdk.org/?repo=jdk&pr=33753&range=00
  Issue: https://bugs.openjdk.org/browse/JDK-8388184
  Stats: 1006 lines in 20 files changed: 459 ins; 260 del; 287 mod
  Patch: https://git.openjdk.org/jdk/pull/33753.diff
  Fetch: git fetch https://git.openjdk.org/jdk.git pull/33753/head:pull/33753

PR: https://git.openjdk.org/jdk/pull/33753

Reply via email to