Move binding to most specific virtual method into Devirtualizer
Bug: 149516194
Change-Id: I26bd69f560c23745e9a2e04ee0498178e713a661
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index 7226924..fc50632 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -27,7 +27,6 @@
import com.android.tools.r8.graph.DexValue.DexValueType;
import com.android.tools.r8.graph.GraphLense;
import com.android.tools.r8.graph.GraphLense.GraphLenseLookupResult;
-import com.android.tools.r8.graph.ResolutionResult;
import com.android.tools.r8.graph.RewrittenPrototypeDescription;
import com.android.tools.r8.graph.RewrittenPrototypeDescription.RemovedArgumentInfoCollection;
import com.android.tools.r8.graph.UseRegistry.MethodHandleUse;
@@ -47,7 +46,6 @@
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionListIterator;
import com.android.tools.r8.ir.code.Invoke;
-import com.android.tools.r8.ir.code.Invoke.Type;
import com.android.tools.r8.ir.code.InvokeCustom;
import com.android.tools.r8.ir.code.InvokeDirect;
import com.android.tools.r8.ir.code.InvokeMethod;
@@ -169,11 +167,6 @@
graphLense.lookupMethod(invokedMethod, method.method, invoke.getType());
DexMethod actualTarget = lenseLookup.getMethod();
Invoke.Type actualInvokeType = lenseLookup.getType();
- if (actualInvokeType == Type.VIRTUAL) {
- actualTarget =
- rebindVirtualInvokeToMostSpecific(
- actualTarget, invoke.inValues().get(0), method.method.holder);
- }
if (actualTarget != invokedMethod || invoke.getType() != actualInvokeType) {
RewrittenPrototypeDescription prototypeChanges =
graphLense.lookupPrototypeChanges(actualTarget);
@@ -598,80 +591,6 @@
return newProto != oldProto ? new DexValueMethodType(newProto) : type;
}
- /**
- * This rebinds invoke-virtual instructions to their most specific target.
- *
- * <p>As a simple example, consider the instruction "invoke-virtual A.foo(v0)", and assume that v0
- * is defined by an instruction "new-instance v0, B". If B is a subtype of A, and B overrides the
- * method foo(), then we rewrite the invocation into "invoke-virtual B.foo(v0)".
- *
- * <p>If A.foo() ends up being unused, this helps to ensure that we can get rid of A.foo()
- * entirely. Without this rewriting, we would have to keep A.foo() because the method is targeted.
- */
- private DexMethod rebindVirtualInvokeToMostSpecific(
- DexMethod target, Value receiver, DexType context) {
- if (!receiver.getTypeLattice().isClassType()) {
- return target;
- }
- DexEncodedMethod encodedTarget = appView.definitionFor(target);
- if (encodedTarget == null
- || !canInvokeTargetWithInvokeVirtual(encodedTarget)
- || !hasAccessToInvokeTargetFromContext(encodedTarget, context)) {
- // Don't rewrite this instruction as it could remove an error from the program.
- return target;
- }
- DexType receiverType =
- appView
- .graphLense()
- .lookupType(receiver.getTypeLattice().asClassTypeLatticeElement().getClassType());
- if (receiverType == target.holder) {
- // Virtual invoke is already as specific as it can get.
- return target;
- }
- ResolutionResult resolutionResult = appView.appInfo().resolveMethod(receiverType, target);
- DexEncodedMethod newTarget =
- resolutionResult.isVirtualTarget() ? resolutionResult.getSingleTarget() : null;
- if (newTarget == null || newTarget.method == target) {
- // Most likely due to a missing class, or invoke is already as specific as it gets.
- return target;
- }
- DexClass newTargetClass = appView.definitionFor(newTarget.method.holder);
- if (newTargetClass == null
- || newTargetClass.isLibraryClass()
- || !canInvokeTargetWithInvokeVirtual(newTarget)
- || !hasAccessToInvokeTargetFromContext(newTarget, context)) {
- // Not safe to invoke `newTarget` with virtual invoke from the current context.
- return target;
- }
- return newTarget.method;
- }
-
- private boolean canInvokeTargetWithInvokeVirtual(DexEncodedMethod target) {
- return target.isNonPrivateVirtualMethod()
- && appView.isInterface(target.method.holder).isFalse();
- }
-
- private boolean hasAccessToInvokeTargetFromContext(DexEncodedMethod target, DexType context) {
- assert !target.accessFlags.isPrivate();
- DexType holder = target.method.holder;
- if (holder == context) {
- // It is always safe to invoke a method from the same enclosing class.
- return true;
- }
- DexClass clazz = appView.definitionFor(holder);
- if (clazz == null) {
- // Conservatively report an illegal access.
- return false;
- }
- if (holder.isSamePackage(context)) {
- // The class must be accessible (note that we have already established that the method is not
- // private).
- return !clazz.accessFlags.isPrivate();
- }
- // If the method is in another package, then the method and its holder must be public.
- return clazz.accessFlags.isPublic() && target.accessFlags.isPublic();
- }
-
class InstructionReplacer {
private final IRCode code;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
index bebbbf4..0c53a4c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
@@ -6,7 +6,9 @@
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ResolutionResult;
import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
import com.android.tools.r8.ir.code.Assume;
@@ -17,6 +19,7 @@
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.Invoke;
import com.android.tools.r8.ir.code.InvokeInterface;
import com.android.tools.r8.ir.code.InvokeVirtual;
import com.android.tools.r8.ir.code.Value;
@@ -31,8 +34,14 @@
import java.util.Set;
/**
- * Rewrites all invoke-interface instructions that have a unique target on a class into
+ * Tries to rewrite virtual invokes to their most specific target by:
+ *
+ * <pre>
+ * 1) Rewriting all invoke-interface instructions that have a unique target on a class into
* invoke-virtual with the corresponding unique target.
+ * 2) Rewriting all invoke-virtual instructions that have a more specific target to an
+ * invoke-virtual with the corresponding target.
+ * </pre>
*/
public class Devirtualizer {
@@ -105,6 +114,20 @@
}
}
+ if (current.isInvokeVirtual()) {
+ InvokeVirtual invoke = current.asInvokeVirtual();
+ DexMethod invokedMethod = invoke.getInvokedMethod();
+ DexMethod reboundTarget =
+ rebindVirtualInvokeToMostSpecific(
+ invokedMethod, invoke.getReceiver(), invocationContext);
+ if (reboundTarget != invokedMethod) {
+ Invoke newInvoke =
+ Invoke.create(
+ Invoke.Type.VIRTUAL, reboundTarget, null, invoke.outValue(), invoke.inValues());
+ it.replaceCurrentInstruction(newInvoke);
+ }
+ }
+
if (!current.isInvokeInterface()) {
continue;
}
@@ -226,4 +249,78 @@
}
assert code.isConsistentSSA();
}
+
+ /**
+ * This rebinds invoke-virtual instructions to their most specific target.
+ *
+ * <p>As a simple example, consider the instruction "invoke-virtual A.foo(v0)", and assume that v0
+ * is defined by an instruction "new-instance v0, B". If B is a subtype of A, and B overrides the
+ * method foo(), then we rewrite the invocation into "invoke-virtual B.foo(v0)".
+ *
+ * <p>If A.foo() ends up being unused, this helps to ensure that we can get rid of A.foo()
+ * entirely. Without this rewriting, we would have to keep A.foo() because the method is targeted.
+ */
+ private DexMethod rebindVirtualInvokeToMostSpecific(
+ DexMethod target, Value receiver, DexType context) {
+ if (!receiver.getTypeLattice().isClassType()) {
+ return target;
+ }
+ DexEncodedMethod encodedTarget = appView.definitionFor(target);
+ if (encodedTarget == null
+ || !canInvokeTargetWithInvokeVirtual(encodedTarget)
+ || !hasAccessToInvokeTargetFromContext(encodedTarget, context)) {
+ // Don't rewrite this instruction as it could remove an error from the program.
+ return target;
+ }
+ DexType receiverType =
+ appView
+ .graphLense()
+ .lookupType(receiver.getTypeLattice().asClassTypeLatticeElement().getClassType());
+ if (receiverType == target.holder) {
+ // Virtual invoke is already as specific as it can get.
+ return target;
+ }
+ ResolutionResult resolutionResult = appView.appInfo().resolveMethod(receiverType, target);
+ DexEncodedMethod newTarget =
+ resolutionResult.isVirtualTarget() ? resolutionResult.getSingleTarget() : null;
+ if (newTarget == null || newTarget.method == target) {
+ // Most likely due to a missing class, or invoke is already as specific as it gets.
+ return target;
+ }
+ DexClass newTargetClass = appView.definitionFor(newTarget.method.holder);
+ if (newTargetClass == null
+ || newTargetClass.isLibraryClass()
+ || !canInvokeTargetWithInvokeVirtual(newTarget)
+ || !hasAccessToInvokeTargetFromContext(newTarget, context)) {
+ // Not safe to invoke `newTarget` with virtual invoke from the current context.
+ return target;
+ }
+ return newTarget.method;
+ }
+
+ private boolean canInvokeTargetWithInvokeVirtual(DexEncodedMethod target) {
+ return target.isNonPrivateVirtualMethod()
+ && appView.isInterface(target.method.holder).isFalse();
+ }
+
+ private boolean hasAccessToInvokeTargetFromContext(DexEncodedMethod target, DexType context) {
+ assert !target.accessFlags.isPrivate();
+ DexType holder = target.method.holder;
+ if (holder == context) {
+ // It is always safe to invoke a method from the same enclosing class.
+ return true;
+ }
+ DexClass clazz = appView.definitionFor(holder);
+ if (clazz == null) {
+ // Conservatively report an illegal access.
+ return false;
+ }
+ if (holder.isSamePackage(context)) {
+ // The class must be accessible (note that we have already established that the method is not
+ // private).
+ return !clazz.accessFlags.isPrivate();
+ }
+ // If the method is in another package, then the method and its holder must be public.
+ return clazz.accessFlags.isPublic() && target.accessFlags.isPublic();
+ }
}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
index 9ccdb7f..9ebb411 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
@@ -70,12 +70,7 @@
// inlined into main(), which makes A.foo() eligible for inlining into main().
ClassSubject aClassSubject = inspector.clazz(A.class);
assertThat(aClassSubject, isPresent());
- if (maxInliningDepth == 0) {
- assertThat(aClassSubject.uniqueMethodWithName("foo"), isPresent());
- } else {
- assert maxInliningDepth == 1;
- assertThat(aClassSubject.uniqueMethodWithName("foo"), not(isPresent()));
- }
+ assertThat(aClassSubject.uniqueMethodWithName("foo"), not(isPresent()));
// A.bar() should always be inlined because it is marked as @AlwaysInline.
assertThat(aClassSubject.uniqueMethodWithName("bar"), not(isPresent()));