Use receiver type to improve CHA decisions

This commit is contained in:
Vladimir Ivanov 2025-12-04 18:58:59 -08:00
parent d054865200
commit 2551b03c42
7 changed files with 205 additions and 99 deletions

View File

@ -526,6 +526,7 @@ bool LateInlineVirtualCallGenerator::do_late_inline_check(Compile* C, JVMState*
jvms,
allow_inline,
_prof_factor,
nullptr /*receiver_type*/,
nullptr /*speculative_receiver_type*/,
true /*allow_intrinsics*/);
@ -1103,11 +1104,12 @@ CallGenerator* CallGenerator::for_method_handle_inline(JVMState* jvms, ciMethod*
int vtable_index = Method::invalid_vtable_index;
bool call_does_dispatch = false;
const TypeOopPtr* receiver_type = nullptr;
ciKlass* speculative_receiver_type = nullptr;
if (is_virtual_or_interface) {
ciInstanceKlass* klass = target->holder();
Node* receiver_node = kit.argument(0);
const TypeOopPtr* receiver_type = gvn.type(receiver_node)->isa_oopptr();
Node* receiver_node = kit.argument(0);
receiver_type = gvn.type(receiver_node)->isa_oopptr();
// call_does_dispatch and vtable_index are out-parameters. They might be changed.
// optimize_virtual_call() takes 2 different holder
// arguments for a corner case that doesn't apply here (see
@ -1123,6 +1125,7 @@ CallGenerator* CallGenerator::for_method_handle_inline(JVMState* jvms, ciMethod*
CallGenerator* cg = C->call_generator(target, vtable_index, call_does_dispatch, jvms,
allow_inline,
PROB_ALWAYS,
receiver_type,
speculative_receiver_type);
return cg;
} else {

View File

@ -981,7 +981,9 @@ public:
// Decide how to build a call.
// The profile factor is a discount to apply to this site's interp. profile.
CallGenerator* call_generator(ciMethod* call_method, int vtable_index, bool call_does_dispatch,
JVMState* jvms, bool allow_inline, float profile_factor, ciKlass* speculative_receiver_type = nullptr,
JVMState* jvms, bool allow_inline, float profile_factor,
const TypeOopPtr* receiver_type = nullptr,
ciKlass* speculative_receiver_type = nullptr,
bool allow_intrinsics = true);
bool should_delay_inlining(ciMethod* call_method, JVMState* jvms) {
return C->directive()->should_delay_inline(call_method) ||

View File

@ -87,8 +87,9 @@ static void trace_type_profile(Compile* C, ciMethod* method, JVMState* jvms,
}
CallGenerator* Compile::call_generator(ciMethod* callee, int vtable_index, bool call_does_dispatch,
JVMState* jvms, bool allow_inline,
float prof_factor, ciKlass* speculative_receiver_type,
JVMState* jvms, bool allow_inline, float prof_factor,
const TypeOopPtr* receiver_type,
ciKlass* speculative_receiver_type,
bool allow_intrinsics) {
assert(callee != nullptr, "failed method resolution");
@ -151,8 +152,8 @@ CallGenerator* Compile::call_generator(ciMethod* callee, int vtable_index, bool
if (cg != nullptr) {
if (cg->is_predicated()) {
// Code without intrinsic but, hopefully, inlined.
CallGenerator* inline_cg = this->call_generator(callee,
vtable_index, call_does_dispatch, jvms, allow_inline, prof_factor, speculative_receiver_type, false);
CallGenerator* inline_cg = this->call_generator(callee, vtable_index, call_does_dispatch, jvms, allow_inline,
prof_factor, receiver_type, speculative_receiver_type, false);
if (inline_cg != nullptr) {
cg = CallGenerator::for_predicated_intrinsic(cg, inline_cg);
}
@ -325,56 +326,64 @@ CallGenerator* Compile::call_generator(ciMethod* callee, int vtable_index, bool
}
// If there is only one implementor of this interface then we
// may be able to bind this invoke directly to the implementing
// klass but we need both a dependence on the single interface
// and on the method we bind to. Additionally since all we know
// may be able to bind this invoke directly to the implementing klass,
// but we need both a dependence on the single interface and
// on the method we bind to. Additionally, since all we know
// about the receiver type is that it's supposed to implement the
// interface we have to insert a check that it's the class we
// expect. Interface types are not checked by the verifier so
// they are roughly equivalent to Object.
// The number of implementors for declared_interface is less or
// equal to the number of implementors for target->holder() so
// if number of implementors of target->holder() == 1 then
// number of implementors for decl_interface is 0 or 1. If
// it's 0 then no class implements decl_interface and there's
// no point in inlining.
// they are roughly equivalent to Object, but receiver type can be used
// to narrow context type beyond declared_interface, so a more specific
// interface with a unique implementor can be used instead.
if (call_does_dispatch && is_interface) {
ciInstanceKlass* declared_interface = nullptr;
ciInstanceKlass* context_intf = nullptr;
// Use declared interface class as a starting point.
if (orig_callee->intrinsic_id() == vmIntrinsics::_linkToInterface) {
// MemberName doesn't keep information about resolved interface class (REFC) once
// resolution is over, but resolved method holder (DECC) can be used as a
// conservative approximation.
declared_interface = callee->holder();
context_intf = callee->holder();
} else {
assert(!orig_callee->is_method_handle_intrinsic(), "not allowed");
declared_interface = caller->get_declared_method_holder_at_bci(bci)->as_instance_klass();
context_intf = caller->get_declared_method_holder_at_bci(bci)->as_instance_klass();
}
assert(declared_interface->is_interface(), "required");
ciInstanceKlass* singleton = declared_interface->unique_implementor();
assert(context_intf->is_interface(), "required");
// Based on receiver info, narrow context to one of most specific superinterfaces with a unique implementor.
if (receiver_type != nullptr && receiver_type->isa_instptr()) {
ciInstanceKlass* intf = receiver_type->is_instptr()->has_unique_implementor(context_intf);
if (intf != nullptr && intf != context_intf) {
assert(intf->is_subtype_of(context_intf), "not related");
assert(intf->unique_implementor() != nullptr, "no unique implementor");
context_intf = intf;
}
}
ciInstanceKlass* singleton = context_intf->unique_implementor();
if (singleton != nullptr) {
assert(singleton != declared_interface, "not a unique implementor");
assert(singleton != context_intf, "not a unique implementor");
ciMethod* cha_monomorphic_target =
callee->find_monomorphic_target(caller->holder(), declared_interface, singleton, check_access);
callee->find_monomorphic_target(caller->holder(), context_intf, singleton, check_access);
if (cha_monomorphic_target != nullptr &&
cha_monomorphic_target->holder() != env()->Object_klass()) { // subtype check against Object is useless
ciKlass* holder = cha_monomorphic_target->holder();
// Try to inline the method found by CHA. Inlined method is guarded by the type check.
CallGenerator* hit_cg = call_generator(cha_monomorphic_target,
vtable_index, !call_does_dispatch, jvms, allow_inline, prof_factor);
CallGenerator* hit_cg = call_generator(cha_monomorphic_target, vtable_index, !call_does_dispatch, jvms,
allow_inline, prof_factor);
// Deoptimize on type check fail. The interpreter will throw ICCE for us.
CallGenerator* miss_cg = CallGenerator::for_uncommon_trap(callee,
Deoptimization::Reason_class_check, Deoptimization::Action_none);
Deoptimization::Reason_class_check,
Deoptimization::Action_none);
ciKlass* constraint = (holder->is_subclass_of(singleton) ? holder : singleton); // avoid upcasts
CallGenerator* cg = CallGenerator::for_guarded_call(constraint, miss_cg, hit_cg);
if (hit_cg != nullptr && cg != nullptr) {
dependencies()->assert_unique_implementor(declared_interface, singleton);
dependencies()->assert_unique_concrete_method(declared_interface, cha_monomorphic_target, declared_interface, callee);
dependencies()->assert_unique_implementor(context_intf, singleton);
dependencies()->assert_unique_concrete_method(context_intf, cha_monomorphic_target, context_intf, callee);
return cg;
}
}
@ -595,10 +604,11 @@ void Parse::do_call() {
bool call_does_dispatch = false;
// Speculative type of the receiver if any
const TypeOopPtr* receiver_type = nullptr;
ciKlass* speculative_receiver_type = nullptr;
if (is_virtual_or_interface) {
Node* receiver_node = stack(sp() - nargs);
const TypeOopPtr* receiver_type = _gvn.type(receiver_node)->isa_oopptr();
Node* receiver_node = stack(sp() - nargs);
receiver_type = _gvn.type(receiver_node)->isa_oopptr();
// call_does_dispatch and vtable_index are out-parameters. They might be changed.
// For arrays, klass below is Object. When vtable calls are used,
// resolving the call with Object would allow an illegal call to
@ -609,7 +619,7 @@ void Parse::do_call() {
callee = C->optimize_virtual_call(method(), klass, holder, orig_callee,
receiver_type, is_virtual,
call_does_dispatch, vtable_index); // out-parameters
speculative_receiver_type = receiver_type != nullptr ? receiver_type->speculative_type() : nullptr;
speculative_receiver_type = (receiver_type != nullptr ? receiver_type->speculative_type() : nullptr);
}
// Additional receiver subtype checks for interface calls via invokespecial or invokeinterface.
@ -655,7 +665,8 @@ void Parse::do_call() {
// Decide call tactic.
// This call checks with CHA, the interpreter profile, intrinsics table, etc.
// It decides whether inlining is desirable or not.
CallGenerator* cg = C->call_generator(callee, vtable_index, call_does_dispatch, jvms, try_inline, prof_factor(), speculative_receiver_type);
CallGenerator* cg = C->call_generator(callee, vtable_index, call_does_dispatch, jvms, try_inline, prof_factor(),
receiver_type, speculative_receiver_type);
// NOTE: Don't use orig_callee and callee after this point! Use cg->method() instead.
orig_callee = callee = nullptr;
@ -700,7 +711,9 @@ void Parse::do_call() {
// the call site, perhaps because it did not match a pattern the
// intrinsic was expecting to optimize. Should always be possible to
// get a normal java call that may inline in that case
cg = C->call_generator(cg->method(), vtable_index, call_does_dispatch, jvms, try_inline, prof_factor(), speculative_receiver_type, /* allow_intrinsics= */ false);
cg = C->call_generator(cg->method(), vtable_index, call_does_dispatch, jvms, try_inline, prof_factor(),
receiver_type, speculative_receiver_type,
false /*allow_intrinsics*/);
new_jvms = cg->generate(jvms);
if (new_jvms == nullptr) {
guarantee(failing(), "call failed to generate: calls should work");

View File

@ -3438,6 +3438,22 @@ bool TypeInterfaces::has_non_array_interface() const {
return !TypeAryPtr::_array_interfaces->contains(this);
}
// Look for a leaf interface with unique implementor under context class. Report one of possibly many.
ciInstanceKlass* TypeInterfaces::has_unique_implementor(ciInstanceKlass* context) const {
assert(context->is_interface(), "not an interface");
ciInstanceKlass* candidate = nullptr;
for (int i = 0; i < _interfaces.length(); i++) {
ciInstanceKlass* intf = _interfaces.at(i);
if (intf->is_subtype_of(context) && (intf->unique_implementor() != nullptr)) {
if (candidate == nullptr || intf->is_subtype_of(candidate)) {
assert(candidate == nullptr || (candidate->unique_implementor() != nullptr), "not a candidate");
candidate = intf;
}
}
}
return candidate;
}
//------------------------------TypeOopPtr-------------------------------------
TypeOopPtr::TypeOopPtr(TYPES t, PTR ptr, ciKlass* k, const TypeInterfaces* interfaces, bool xk, ciObject* o, int offset,
int instance_id, const TypePtr* speculative, int inline_depth)
@ -4547,6 +4563,19 @@ const TypeKlassPtr* TypeInstPtr::as_klass_type(bool try_for_exact) const {
return TypeInstKlassPtr::make(xk ? TypePtr::Constant : TypePtr::NotNull, klass(), _interfaces, 0);
}
// Does the type represent an interface instance?
bool TypeInstPtr::is_interface() const {
return (instance_klass() == ciEnv::current()->Object_klass()) && !_interfaces->empty();
}
// For an interface instance reports one of most specific superinterfaces with a unique implementor.
ciInstanceKlass* TypeInstPtr::has_unique_implementor(ciInstanceKlass* context_intf) const {
if (is_interface() && context_intf->is_interface()) {
return _interfaces->has_unique_implementor(context_intf);
}
return nullptr;
}
template <class T1, class T2> bool TypePtr::is_meet_subtype_of_helper_for_instance(const T1* this_one, const T2* other, bool this_xk, bool other_xk) {
static_assert(std::is_base_of<T2, T1>::value, "");

View File

@ -1128,6 +1128,8 @@ public:
bool singleton(void) const;
bool has_non_array_interface() const;
ciInstanceKlass* has_unique_implementor(ciInstanceKlass* context) const;
};
//------------------------------TypePtr----------------------------------------
@ -1585,6 +1587,11 @@ public:
const TypeKlassPtr* as_klass_type(bool try_for_exact = false) const;
bool is_interface() const;
// Find one (of possibly many) most specific superinterfaces with a unique implementor.
ciInstanceKlass* has_unique_implementor(ciInstanceKlass* context_intf) const;
// Convenience common pre-built types.
static const TypeInstPtr *NOTNULL;
static const TypeInstPtr *BOTTOM;

View File

@ -23,9 +23,7 @@
/*
* @test
* @requires !vm.graal.enabled
* @requires vm.opt.StressMethodHandleLinkerInlining == null | !vm.opt.StressMethodHandleLinkerInlining
* @requires vm.opt.StressUnstableIfTraps == null | !vm.opt.StressUnstableIfTraps
* @requires vm.flagless
* @modules java.base/jdk.internal.misc
* java.base/jdk.internal.vm.annotation
* @library /testlibrary/asm
@ -58,8 +56,10 @@ import jdk.internal.vm.annotation.DontInline;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.concurrent.Callable;
import static compiler.cha.Utils.*;
import static jdk.test.lib.Asserts.assertTrue;
public class StrengthReduceInterfaceCall {
public static void main(String[] args) {
@ -82,6 +82,11 @@ public class StrengthReduceInterfaceCall {
run(ThreeLevelDefaultHierarchy1.TestMH.class, ThreeLevelDefaultHierarchy1.class);
}
// Receiver type information is only available in C2.
if (!jdk.test.whitebox.code.Compiler.isC1Enabled()) {
run(MultipleInterfacesOnReceiver.class);
run(MultipleInterfacesOnReceiver.TestMH.class, MultipleInterfacesOnReceiver.class);
}
System.out.println("TEST PASSED");
}
@ -462,16 +467,10 @@ public class StrengthReduceInterfaceCall {
assertCompiled(); // No deopt on not-yet-seen receiver
// 2. No dependency invalidation: different context
if (contextClass() == I.class) {
initialize(DJ.class, // DJ.m <: intf J.m ABSTRACT
K1.class, // intf K1 <: intf I <: intf J.m ABSTRACT
K2.class); // intf K2.m ABSTRACT <: intf I <: intf J.m ABSTRACT
assertCompiled();
} else if (contextClass() == J.class) {
// no classes to initialize w/o breaking a dependency
} else {
throw new InternalError("unsupported context: " + contextClass());
}
initialize(DJ.class, // DJ.m <: intf J.m ABSTRACT
K1.class, // intf K1 <: intf I <: intf J.m ABSTRACT
K2.class); // intf K2.m ABSTRACT <: intf I <: intf J.m ABSTRACT
assertCompiled();
// 3. Dependency invalidation: DI.m <: I
initialize(DI.class); // DI.m <: intf I <: intf J.m ABSTRACT
@ -556,10 +555,6 @@ public class StrengthReduceInterfaceCall {
assertCompiled();
}
Class<?> contextClass() {
return I.class;
}
public static class TestMH extends ThreeLevelHierarchyLinear {
static final MethodHandle TEST_MH = findVirtualHelper(I.class, "m", Object.class, MethodHandles.lookup());
@ -568,11 +563,6 @@ public class StrengthReduceInterfaceCall {
// receiver type check failures trigger nmethod invalidation
}
@Override
Class<?> contextClass() {
return J.class;
}
@Override
public Object test(I obj) throws Throwable {
return TEST_MH.invokeExact(obj); // invokeinterface I.m()
@ -738,17 +728,11 @@ public class StrengthReduceInterfaceCall {
assertCompiled();
// 2. No dependency invalidation
if (contextClass() == I.class) {
initialize(DJ.class, // DJ.m <: intf J.m ABSTRACT
K1.class, // intf K1 <: intf I <: intf J.m ABSTRACT
K2.class, // intf K2.m ABSTRACT <: intf I <: intf J.m ABSTRACT
DK3.class); // DK3.m <: intf K3.m DEFAULT <: intf J.m ABSTRACT
assertCompiled();
} else if (contextClass() == J.class) {
// no classes to initialize w/o breaking a dependency
} else {
throw new InternalError("unsupported context: " + contextClass());
}
initialize(DJ.class, // DJ.m <: intf J.m ABSTRACT
K1.class, // intf K1 <: intf I <: intf J.m ABSTRACT
K2.class, // intf K2.m ABSTRACT <: intf I <: intf J.m ABSTRACT
DK3.class); // DK3.m <: intf K3.m DEFAULT <: intf J.m ABSTRACT
assertCompiled();
// 3. Dependency invalidation
initialize(DI.class); // DI.m <: intf I <: intf J.m ABSTRACT
@ -775,10 +759,6 @@ public class StrengthReduceInterfaceCall {
assertCompiled();
}
Class<?> contextClass() {
return I.class;
}
public static class TestMH extends ThreeLevelDefaultHierarchy {
static final MethodHandle TEST_MH = findVirtualHelper(I.class, "m", Object.class, MethodHandles.lookup());
@ -787,11 +767,6 @@ public class StrengthReduceInterfaceCall {
// receiver type check failures trigger nmethod invalidation
}
@Override
Class<?> contextClass() {
return J.class;
}
@Override
public Object test(I obj) throws Throwable {
return TEST_MH.invokeExact(obj); // invokeinterface I.m()
@ -836,20 +811,11 @@ public class StrengthReduceInterfaceCall {
assertCompiled();
// 2. No dependency invalidation
if (contextClass() == I.class) {
initialize(DJ1.class, // DJ1.m <: intf J1
DJ2.class, // DJ2.m <: intf J2.m ABSTRACT
K1.class, // intf K1 <: intf I <: intf J1, intf J2.m ABSTRACT
K2.class, // intf K2.m ABSTRACT <: intf I <: intf J1, intf J2.m ABSTRACT
K3.class); // intf K3.m DEFAULT <: intf I <: intf J1, intf J2.m ABSTRACT
} else if (contextClass() == J2.class) {
initialize(DJ1.class, // DJ1.m <: intf J1
K1.class, // intf K1 <: intf I <: intf J1, intf J2.m ABSTRACT
K2.class, // intf K2.m ABSTRACT <: intf I <: intf J1, intf J2.m ABSTRACT
K3.class); // intf K3.m DEFAULT <: intf I <: intf J1, intf J2.m ABSTRACT
} else {
throw new InternalError("unsupported context: " + contextClass());
}
initialize(DJ1.class, // DJ1.m <: intf J1
DJ2.class, // DJ2.m <: intf J2.m ABSTRACT
K1.class, // intf K1 <: intf I <: intf J1, intf J2.m ABSTRACT
K2.class, // intf K2.m ABSTRACT <: intf I <: intf J1, intf J2.m ABSTRACT
K3.class); // intf K3.m DEFAULT <: intf I <: intf J1, intf J2.m ABSTRACT
assertCompiled();
// 3. Dependency invalidation
@ -883,10 +849,6 @@ public class StrengthReduceInterfaceCall {
assertCompiled();
}
Class<?> contextClass() {
return I.class;
}
public static class TestMH extends ThreeLevelDefaultHierarchy1 {
static final MethodHandle TEST_MH = findVirtualHelper(I.class, "m", Object.class, MethodHandles.lookup());
@ -895,15 +857,100 @@ public class StrengthReduceInterfaceCall {
// receiver type check failures trigger nmethod invalidation
}
@Override
Class<?> contextClass() {
return J2.class;
}
@Override
public Object test(I obj) throws Throwable {
return TEST_MH.invokeExact(obj); // invokeinterface I.m()
}
}
}
public static class MultipleInterfacesOnReceiver extends ATest<MultipleInterfacesOnReceiver.I> {
public MultipleInterfacesOnReceiver() { super(I.class, D.class); }
interface L1 {}
interface L2 {}
interface J { Object m(); }
interface I extends J {}
interface I1 extends I {}
interface I2 extends I {}
interface I3 extends I {}
interface I4 extends I {}
interface K1 extends I1 {}
interface K2 extends I2 {}
interface K3 extends I3 {}
interface K4 extends I4 {}
static class C implements I { public Object m() { return WRONG; }}
static class D implements K1, K2, K3, K4, L1, L2 { public Object m() { return CORRECT; }}
static class F1 implements K1 { public Object m() { return WRONG; }}
static class F2 implements K2 { public Object m() { return WRONG; }}
static class F3 implements K3 { public Object m() { return WRONG; }}
static class F4 implements K4 { public Object m() { return WRONG; }}
@DontInline
public Object test(I i) throws Throwable {
// K1, K2, K3, and K4 are interfaces with unique implementor D until F1, ..., F4 are loaded.
if (i instanceof K1 && i instanceof K2 && i instanceof K3 && i instanceof K4 &&
i instanceof L1 && i instanceof L2) {
return i.m(); // I <: J.m ABSTRACT; i <: (K1 & K2 & K3 & K4 & L1 & L2)
} else {
return WRONG;
}
}
@TestCase
public void testMega() throws Exception {
assertTrue(!jdk.test.whitebox.code.Compiler.isC1Enabled(), "C2 only");
initialize(C.class); // exclude I
// NB! Avoid accessing context classes directly to delay their loading until context invalidation.
for (Callable<?> context : new Callable[] { () -> F1.class, () -> F2.class,
() -> F3.class, () -> F4.class }) {
// Trigger compilation of a megamorphic call site
resetCompiledState();
compile(megamorphic());
assertCompiled();
// Dependency: type = unique_concrete_method, context = F1...F4, method = D.m
initialize((Class<?>)context.call()); // context class doesn't have a unique implementor anymore,
// but we can't say which one is chosen as a context by the JVM
}
assertNotCompiled();
compile(megamorphic());
assertCompiled();
initialize(L1.class, L2.class); // unrelated interfaces
assertCompiled();
repeat(100, () -> call(new D() {})); // new implementor
assertCompiled();
}
@Override
public void checkInvalidReceiver() {
// nothing to check; test has instanceof guards
}
public static class TestMH extends MultipleInterfacesOnReceiver {
static final MethodHandle TEST_MH = findVirtualHelper(I.class, "m", Object.class, MethodHandles.lookup());
@Override
public Object test(I i) throws Throwable {
if (i instanceof K1 && i instanceof K2 && i instanceof K3 && i instanceof K4 &&
i instanceof L1 && i instanceof L2) {
return TEST_MH.invokeExact(i); // invokeinterface I.m(); i <: (K1 & K2 & K3 & K4 & L1 & L2)
} else {
return WRONG;
}
}
}
}
}

View File

@ -158,6 +158,11 @@ public class Utils {
prevNM = curNM; // update nmethod info
}
public void resetCompiledState() {
prevNM = null;
}
@Override
public void call(T i) {
try {