Extend bridge removal to redundant forwarding constructors
This is still disabled by default (controlled by `enableRedundantConstructorBridgeRemoval`).
The optimization leads to non-rebound constructor calls, which are not always correctly rewritten when there are lens rewritings.
Change-Id: I77b650a9ba05d552c12b795517e8523549070f66
diff --git a/src/main/java/com/android/tools/r8/optimize/InvokeSingleTargetExtractor.java b/src/main/java/com/android/tools/r8/optimize/InvokeSingleTargetExtractor.java
index 77ea56c..11e1377 100644
--- a/src/main/java/com/android/tools/r8/optimize/InvokeSingleTargetExtractor.java
+++ b/src/main/java/com/android/tools/r8/optimize/InvokeSingleTargetExtractor.java
@@ -55,7 +55,7 @@
@Override
public void registerInvokeDirect(DexMethod method) {
- invalid();
+ setTarget(method, InvokeKind.DIRECT);
}
@Override
@@ -109,6 +109,7 @@
}
public enum InvokeKind {
+ DIRECT,
VIRTUAL,
STATIC,
SUPER,
diff --git a/src/main/java/com/android/tools/r8/optimize/RedundantBridgeRemover.java b/src/main/java/com/android/tools/r8/optimize/RedundantBridgeRemover.java
index ff42b4c..6c2af55 100644
--- a/src/main/java/com/android/tools/r8/optimize/RedundantBridgeRemover.java
+++ b/src/main/java/com/android/tools/r8/optimize/RedundantBridgeRemover.java
@@ -11,11 +11,11 @@
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.graph.PrunedItems;
-import com.android.tools.r8.logging.Log;
+import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
import com.android.tools.r8.optimize.InvokeSingleTargetExtractor.InvokeKind;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.google.common.collect.Sets;
-import java.util.Set;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@@ -28,15 +28,16 @@
this.appView = appView;
}
- private boolean isUnneededVisibilityBridge(ProgramMethod method) {
+ private boolean isRedundantBridge(ProgramMethod method) {
// Clean-up the predicate check.
if (appView.appInfo().isPinned(method.getReference())) {
return false;
}
DexEncodedMethod definition = method.getDefinition();
- // TODO(b/198133259): Extend to definitions that are not defined as bridges.
// TODO(b/197490164): Remove if method is abstract.
- if (!definition.isBridge() || definition.isAbstract()) {
+ BridgeInfo bridgeInfo = definition.getOptimizationInfo().getBridgeInfo();
+ boolean isBridge = definition.isBridge() || bridgeInfo != null;
+ if (!isBridge || definition.isAbstract()) {
return false;
}
InvokeSingleTargetExtractor targetExtractor = new InvokeSingleTargetExtractor(appView, method);
@@ -47,7 +48,6 @@
if (target == null || !target.match(method.getReference())) {
return false;
}
- assert !definition.isPrivate() && !definition.isInstanceInitializer();
if (!isTargetingSuperMethod(method, targetExtractor.getKind(), target)) {
return false;
}
@@ -57,9 +57,22 @@
.appInfo()
.unsafeResolveMethodDueToDexFormatLegacy(target)
.getResolvedProgramMethod();
- if (targetMethod == null || !targetMethod.getAccessFlags().isPublic()) {
+ if (targetMethod == null) {
return false;
}
+ if (method.getAccessFlags().isPublic()) {
+ if (!targetMethod.getAccessFlags().isPublic()) {
+ return false;
+ }
+ } else {
+ if (targetMethod.getAccessFlags().isProtected()
+ && !targetMethod.getHolderType().isSamePackage(method.getHolderType())) {
+ return false;
+ }
+ if (targetMethod.getAccessFlags().isPrivate()) {
+ return false;
+ }
+ }
if (definition.isStatic()
&& method.getHolder().hasClassInitializer()
&& method
@@ -67,13 +80,6 @@
.classInitializationMayHaveSideEffectsInContext(appView, targetMethod)) {
return false;
}
- if (Log.ENABLED) {
- Log.info(
- getClass(),
- "Removing visibility forwarding %s -> %s",
- method,
- targetMethod.getReference());
- }
return true;
}
@@ -81,6 +87,14 @@
if (kind == InvokeKind.ILLEGAL) {
return false;
}
+ if (kind == InvokeKind.DIRECT) {
+ return method.getDefinition().isInstanceInitializer()
+ && appView.options().canHaveNonReboundConstructorInvoke()
+ && appView.testing().enableRedundantConstructorBridgeRemoval
+ && appView.appInfo().isStrictSubtypeOf(method.getHolderType(), target.getHolderType());
+ }
+ assert !method.getAccessFlags().isPrivate();
+ assert !method.getDefinition().isInstanceInitializer();
if (kind == InvokeKind.SUPER) {
return true;
}
@@ -92,33 +106,42 @@
}
public void run(ExecutorService executorService) throws ExecutionException {
- // Collect all visibility bridges to remove.
- if (!appView.options().enableVisibilityBridgeRemoval) {
- return;
- }
- ConcurrentHashMap<DexProgramClass, Set<DexEncodedMethod>> visibilityBridgesToRemove =
- new ConcurrentHashMap<>();
+ // Collect all redundant bridges to remove.
+ Map<DexProgramClass, ProgramMethodSet> bridgesToRemove =
+ computeBridgesToRemove(executorService);
+ pruneApp(bridgesToRemove, executorService);
+ }
+
+ private Map<DexProgramClass, ProgramMethodSet> computeBridgesToRemove(
+ ExecutorService executorService) throws ExecutionException {
+ Map<DexProgramClass, ProgramMethodSet> bridgesToRemove = new ConcurrentHashMap<>();
processItems(
appView.appInfo().classes(),
clazz -> {
- Set<DexEncodedMethod> bridgesToRemoveForClass = Sets.newIdentityHashSet();
+ ProgramMethodSet bridgesToRemoveForClass = ProgramMethodSet.create();
clazz.forEachProgramMethod(
method -> {
- if (isUnneededVisibilityBridge(method)) {
- bridgesToRemoveForClass.add(method.getDefinition());
+ if (isRedundantBridge(method)) {
+ bridgesToRemoveForClass.add(method);
}
});
if (!bridgesToRemoveForClass.isEmpty()) {
- visibilityBridgesToRemove.put(clazz, bridgesToRemoveForClass);
+ bridgesToRemove.put(clazz, bridgesToRemoveForClass);
}
},
executorService);
- // Remove all bridges found.
- PrunedItems.Builder builder = PrunedItems.builder();
- visibilityBridgesToRemove.forEach(
+ return bridgesToRemove;
+ }
+
+ private void pruneApp(
+ Map<DexProgramClass, ProgramMethodSet> bridgesToRemove, ExecutorService executorService)
+ throws ExecutionException {
+ PrunedItems.Builder prunedItemsBuilder = PrunedItems.builder().setPrunedApp(appView.app());
+ bridgesToRemove.forEach(
(clazz, methods) -> {
- clazz.getMethodCollection().removeMethods(methods);
- methods.forEach(method -> builder.addRemovedMethod(method.getReference()));
+ clazz.getMethodCollection().removeMethods(methods.toDefinitionSet());
+ methods.forEach(method -> prunedItemsBuilder.addRemovedMethod(method.getReference()));
});
+ appView.pruneItems(prunedItemsBuilder.build(), executorService);
}
}
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 90b9dfb..bbbe4c2 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -2508,39 +2508,41 @@
private void handleInvokeOfDirectTarget(
DexMethod reference, ProgramDefinition context, KeepReason reason) {
- DexType holder = reference.holder;
- DexProgramClass clazz = getProgramClassOrNull(holder, context);
- if (clazz == null) {
- recordMethodReference(reference, context);
- return;
- }
- // TODO(zerny): Is it ok that we lookup in both the direct and virtual pool here?
- DexEncodedMethod encodedMethod = clazz.lookupMethod(reference);
- if (encodedMethod == null) {
- failedMethodResolutionTargets.add(reference);
- return;
- }
+ resolveMethod(reference, context, reason)
+ .forEachMethodResolutionResult(
+ resolutionResult -> {
+ if (resolutionResult.isFailedResolution()) {
+ failedMethodResolutionTargets.add(reference);
+ return;
+ }
- ProgramMethod method = new ProgramMethod(clazz, encodedMethod);
+ if (!resolutionResult.isSingleResolution()
+ || !resolutionResult.getResolvedHolder().isProgramClass()) {
+ return;
+ }
- // We have to mark the resolved method as targeted even if it cannot actually be invoked
- // to make sure the invocation will keep failing in the appropriate way.
- markMethodAsTargeted(method, reason);
+ ProgramMethod resolvedMethod =
+ resolutionResult.asSingleResolution().getResolvedProgramMethod();
- // Only mark methods for which invocation will succeed at runtime live.
- if (encodedMethod.isStatic()) {
- return;
- }
+ // We have to mark the resolved method as targeted even if it cannot actually be
+ // invoked to make sure the invocation will keep failing in the appropriate way.
+ markMethodAsTargeted(resolvedMethod, reason);
- markDirectStaticOrConstructorMethodAsLive(method, reason);
+ // Only mark methods for which invocation will succeed at runtime live.
+ if (resolvedMethod.getAccessFlags().isStatic()) {
+ return;
+ }
- // It is valid to have an invoke-direct instruction in a default interface method that
- // targets another default method in the same interface (see testInvokeSpecialToDefault-
- // Method). In a class, that would lead to a verification error.
- if (encodedMethod.isNonPrivateVirtualMethod()
- && virtualMethodsTargetedByInvokeDirect.add(encodedMethod.getReference())) {
- workList.enqueueMarkMethodLiveAction(method, context, reason);
- }
+ markDirectStaticOrConstructorMethodAsLive(resolvedMethod, reason);
+
+ // It is valid to have an invoke-direct instruction in a default interface method that
+ // targets another default method in the same interface. In a class, that would lead
+ // to a verification error. See also testInvokeSpecialToDefaultMethod.
+ if (resolvedMethod.getDefinition().isNonPrivateVirtualMethod()
+ && virtualMethodsTargetedByInvokeDirect.add(resolvedMethod.getReference())) {
+ workList.enqueueMarkMethodLiveAction(resolvedMethod, context, reason);
+ }
+ });
}
private void ensureFromLibraryOrThrow(DexType type, DexLibraryClass context) {
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 9c2c3b1..70495fb 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -347,7 +347,6 @@
public boolean readDebugSetFileEvent = false;
public boolean disableL8AnnotationRemoval =
System.getProperty("com.android.tools.r8.disableL8AnnotationRemoval") != null;
- public boolean enableVisibilityBridgeRemoval = true;
public int callGraphLikelySpuriousCallEdgeThreshold = 50;
@@ -1920,6 +1919,7 @@
public boolean enableDeadSwitchCaseElimination = true;
public boolean enableInvokeSuperToInvokeVirtualRewriting = true;
public boolean enableMultiANewArrayDesugaringForClassFiles = false;
+ public boolean enableRedundantConstructorBridgeRemoval = false;
public boolean enableSwitchToIfRewriting = true;
public boolean enableEnumUnboxingDebugLogs =
System.getProperty("com.android.tools.r8.enableEnumUnboxingDebugLogs") != null;
@@ -2702,4 +2702,8 @@
public boolean canHaveDalvikEmptyAnnotationSetBug() {
return canHaveBugPresentUntil(AndroidApiLevel.J_MR1);
}
+
+ public boolean canHaveNonReboundConstructorInvoke() {
+ return isGeneratingDex() && minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.L);
+ }
}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/StaticFieldClassInitMemberRebindingTest.java b/src/test/java/com/android/tools/r8/memberrebinding/StaticFieldClassInitMemberRebindingTest.java
index c31c93d..7ab14c7 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/StaticFieldClassInitMemberRebindingTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/StaticFieldClassInitMemberRebindingTest.java
@@ -47,7 +47,6 @@
.setMinApi(parameters.getApiLevel())
.addKeepMainRule(Main.class)
.enableInliningAnnotations()
- .addOptionsModification(options -> options.enableVisibilityBridgeRemoval = false)
.run(parameters.getRuntime(), Main.class)
// TODO(b/220668540): R8 should not change class loading semantics.
.assertSuccessWithOutputLines(R8_EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/shaking/ForwardingConstructorShakingOnDexTest.java b/src/test/java/com/android/tools/r8/shaking/ForwardingConstructorShakingOnDexTest.java
index 78d2096..6ff7000 100644
--- a/src/test/java/com/android/tools/r8/shaking/ForwardingConstructorShakingOnDexTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ForwardingConstructorShakingOnDexTest.java
@@ -14,6 +14,7 @@
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
@@ -39,6 +40,8 @@
testForR8(parameters.getBackend())
.addInnerClasses(getClass())
.addKeepMainRule(Main.class)
+ .addOptionsModification(
+ options -> options.testing.enableRedundantConstructorBridgeRemoval = true)
.enableConstantArgumentAnnotations()
.enableNeverClassInliningAnnotations()
.enableNoVerticalClassMergingAnnotations()
@@ -50,19 +53,25 @@
}
private void inspect(CodeInspector inspector) {
+ boolean canHaveNonReboundConstructorInvoke =
+ parameters.isDexRuntime()
+ && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L);
+
ClassSubject aClassSubject = inspector.clazz(A.class);
assertThat(aClassSubject, isPresent());
assertEquals(2, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
ClassSubject bClassSubject = inspector.clazz(B.class);
assertThat(bClassSubject, isPresent());
- // TODO(b/173751869): Should be 0 when compiling for dex and API is above Dalvik.
- assertEquals(2, bClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+ assertEquals(
+ canHaveNonReboundConstructorInvoke ? 0 : 2,
+ bClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
ClassSubject cClassSubject = inspector.clazz(C.class);
assertThat(cClassSubject, isPresent());
- // TODO(b/173751869): Should be 0 when compiling for dex and API is above Dalvik.
- assertEquals(2, cClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+ assertEquals(
+ canHaveNonReboundConstructorInvoke ? 0 : 2,
+ cClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
}
static class Main {
diff --git a/src/test/java/com/android/tools/r8/shaking/RetainIndirectlyReferencedConstructorShakingOnDexTest.java b/src/test/java/com/android/tools/r8/shaking/RetainIndirectlyReferencedConstructorShakingOnDexTest.java
index 172249c..f914a87 100644
--- a/src/test/java/com/android/tools/r8/shaking/RetainIndirectlyReferencedConstructorShakingOnDexTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/RetainIndirectlyReferencedConstructorShakingOnDexTest.java
@@ -12,6 +12,7 @@
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
@@ -37,6 +38,8 @@
testForR8(parameters.getBackend())
.addInnerClasses(getClass())
.addKeepMainRule(Main.class)
+ .addOptionsModification(
+ options -> options.testing.enableRedundantConstructorBridgeRemoval = true)
.enableNoVerticalClassMergingAnnotations()
.setMinApi(parameters.getApiLevel())
.compile()
@@ -46,6 +49,10 @@
}
private void inspect(CodeInspector inspector) {
+ boolean canHaveNonReboundConstructorInvoke =
+ parameters.isDexRuntime()
+ && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L);
+
// A.<init> should be retained despite the fact that there is no invoke-direct in the program
// that directly targets A.<init> when B.<init> is removed.
ClassSubject aClassSubject = inspector.clazz(A.class);
@@ -54,8 +61,9 @@
ClassSubject bClassSubject = inspector.clazz(B.class);
assertThat(bClassSubject, isPresent());
- // TODO(b/173751869): Should be 0 when compiling for dex and API is above Dalvik.
- assertEquals(1, bClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+ assertEquals(
+ canHaveNonReboundConstructorInvoke ? 0 : 1,
+ bClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
}
static class Main {