Removal of companion class instantiation after method staticizing
Change-Id: I82efe364703c5df76ae351c43d2a6e8db23b777a
diff --git a/src/main/java/com/android/tools/r8/graph/bytecodemetadata/BytecodeInstructionMetadata.java b/src/main/java/com/android/tools/r8/graph/bytecodemetadata/BytecodeInstructionMetadata.java
index 97839cf..5d40177 100644
--- a/src/main/java/com/android/tools/r8/graph/bytecodemetadata/BytecodeInstructionMetadata.java
+++ b/src/main/java/com/android/tools/r8/graph/bytecodemetadata/BytecodeInstructionMetadata.java
@@ -4,6 +4,11 @@
package com.android.tools.r8.graph.bytecodemetadata;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.analysis.fieldaccess.TrivialFieldAccessReprocessor;
+import java.util.Collections;
+import java.util.Set;
+
/**
* A piece of information that can be attached to instructions in {@link
* com.android.tools.r8.graph.CfCode} and {@link com.android.tools.r8.graph.DexCode}.
@@ -11,6 +16,16 @@
public class BytecodeInstructionMetadata {
/**
+ * Set for field-get instructions that are only used to invoke one or more instance methods.
+ *
+ * <p>This is used to skip field-get instructions that will be pruned as a result of method
+ * staticizing in the {@link TrivialFieldAccessReprocessor}. A common use case for this is Kotlin
+ * companion fields, which will generally not be read after the methods of a given companion class
+ * have been staticized.
+ */
+ private final Set<DexMethod> isReadForInvokeReceiver;
+
+ /**
* Set for instance and static field read instructions which are only used to write the same
* field.
*
@@ -19,7 +34,9 @@
*/
private final boolean isReadForWrite;
- BytecodeInstructionMetadata(boolean isReadForWrite) {
+ private BytecodeInstructionMetadata(
+ Set<DexMethod> isReadForInvokeReceiver, boolean isReadForWrite) {
+ this.isReadForInvokeReceiver = isReadForInvokeReceiver;
this.isReadForWrite = isReadForWrite;
}
@@ -31,16 +48,32 @@
return null;
}
+ public boolean isReadForInvokeReceiver() {
+ return isReadForInvokeReceiver != null;
+ }
+
+ public Set<DexMethod> getReadForInvokeReceiver() {
+ return isReadForInvokeReceiver;
+ }
+
public boolean isReadForWrite() {
return isReadForWrite;
}
public static class Builder {
+ private Set<DexMethod> isReadForInvokeReceiver = Collections.emptySet();
private boolean isReadForWrite;
private boolean isEmpty() {
- return !isReadForWrite;
+ return isReadForInvokeReceiver.isEmpty() && !isReadForWrite;
+ }
+
+ public Builder setIsReadForInvokeReceiver(Set<DexMethod> isReadForInvokeReceiver) {
+ assert isReadForInvokeReceiver != null;
+ assert !isReadForInvokeReceiver.isEmpty();
+ this.isReadForInvokeReceiver = isReadForInvokeReceiver;
+ return this;
}
public Builder setIsReadForWrite() {
@@ -50,7 +83,7 @@
public BytecodeInstructionMetadata build() {
assert !isEmpty();
- return new BytecodeInstructionMetadata(isReadForWrite);
+ return new BytecodeInstructionMetadata(isReadForInvokeReceiver, isReadForWrite);
}
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java
index e5a1aec..3ebd37a 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java
@@ -28,6 +28,7 @@
private final AppView<? extends AppInfoWithClassHierarchy> appView;
private final FieldAssignmentTracker fieldAssignmentTracker;
private final FieldBitAccessAnalysis fieldBitAccessAnalysis;
+ private final FieldReadForInvokeReceiverAnalysis fieldReadForInvokeReceiverAnalysis;
private final FieldReadForWriteAnalysis fieldReadForWriteAnalysis;
public FieldAccessAnalysis(AppView<AppInfoWithLiveness> appView) {
@@ -36,6 +37,7 @@
this.fieldBitAccessAnalysis =
options.enableFieldBitAccessAnalysis ? new FieldBitAccessAnalysis() : null;
this.fieldAssignmentTracker = new FieldAssignmentTracker(appView);
+ this.fieldReadForInvokeReceiverAnalysis = new FieldReadForInvokeReceiverAnalysis(appView);
this.fieldReadForWriteAnalysis = new FieldReadForWriteAnalysis(appView);
}
@@ -44,10 +46,12 @@
AppView<? extends AppInfoWithClassHierarchy> appView,
FieldAssignmentTracker fieldAssignmentTracker,
FieldBitAccessAnalysis fieldBitAccessAnalysis,
+ FieldReadForInvokeReceiverAnalysis fieldReadForInvokeReceiverAnalysis,
FieldReadForWriteAnalysis fieldReadForWriteAnalysis) {
this.appView = appView;
this.fieldAssignmentTracker = fieldAssignmentTracker;
this.fieldBitAccessAnalysis = fieldBitAccessAnalysis;
+ this.fieldReadForInvokeReceiverAnalysis = fieldReadForInvokeReceiverAnalysis;
this.fieldReadForWriteAnalysis = fieldReadForWriteAnalysis;
}
@@ -91,6 +95,10 @@
fieldBitAccessAnalysis.recordFieldAccess(
fieldInstruction, field.getDefinition(), feedback);
}
+ if (fieldReadForInvokeReceiverAnalysis != null) {
+ fieldReadForInvokeReceiverAnalysis.recordFieldAccess(
+ fieldInstruction, field, bytecodeMetadataProviderBuilder, code.context());
+ }
if (fieldReadForWriteAnalysis != null) {
fieldReadForWriteAnalysis.recordFieldAccess(
fieldInstruction, field, bytecodeMetadataProviderBuilder);
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForInvokeReceiverAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForInvokeReceiverAnalysis.java
new file mode 100644
index 0000000..b2aded5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldReadForInvokeReceiverAnalysis.java
@@ -0,0 +1,90 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.analysis.fieldaccess;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
+import com.android.tools.r8.ir.code.FieldInstruction;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.StaticGet;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.WorkList;
+import com.google.common.collect.Sets;
+import java.util.Set;
+
+public class FieldReadForInvokeReceiverAnalysis {
+
+ private final AppView<AppInfoWithLiveness> appView;
+
+ FieldReadForInvokeReceiverAnalysis(AppView<AppInfoWithLiveness> appView) {
+ this.appView = appView;
+ }
+
+ public void recordFieldAccess(
+ FieldInstruction instruction,
+ ProgramField field,
+ BytecodeMetadataProvider.Builder bytecodeMetadataProviderBuilder,
+ ProgramMethod context) {
+ if (!instruction.isStaticGet()) {
+ return;
+ }
+
+ StaticGet staticGet = instruction.asStaticGet();
+ Set<DexMethod> methods = getMethods(staticGet.outValue(), context);
+ if (methods == null || methods.isEmpty()) {
+ return;
+ }
+
+ bytecodeMetadataProviderBuilder.addMetadata(
+ instruction, builder -> builder.setIsReadForInvokeReceiver(methods));
+ }
+
+ private Set<DexMethod> getMethods(Value value, ProgramMethod context) {
+ WorkList<Instruction> users = WorkList.newIdentityWorkList();
+ if (!enqueueUsersForAnalysis(value, users)) {
+ return null;
+ }
+ Set<DexMethod> methods = Sets.newIdentityHashSet();
+ while (users.hasNext()) {
+ Instruction user = users.next();
+ if (user.isAssume()) {
+ if (enqueueUsersForAnalysis(user.outValue(), users)) {
+ // OK.
+ continue;
+ }
+ } else if (user.isInvokeMethodWithReceiver()) {
+ InvokeMethodWithReceiver invoke = user.asInvokeMethodWithReceiver();
+ for (int argumentIndex = 1; argumentIndex < invoke.arguments().size(); argumentIndex++) {
+ if (invoke.getArgument(argumentIndex).getAliasedValue() == value) {
+ return null;
+ }
+ }
+ assert invoke.getReceiver().getAliasedValue() == value;
+
+ ProgramMethod singleTarget = invoke.lookupSingleProgramTarget(appView, context);
+ if (singleTarget == null) {
+ return null;
+ }
+
+ methods.add(singleTarget.getReference());
+ }
+ return null;
+ }
+ return methods;
+ }
+
+ private boolean enqueueUsersForAnalysis(Value value, WorkList<Instruction> users) {
+ if (value.hasDebugUsers() || value.hasPhiUsers()) {
+ return false;
+ }
+ users.addIfNotSeen(value.uniqueUsers());
+ return true;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
index f392dec..7f36c0c 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
@@ -4,7 +4,9 @@
package com.android.tools.r8.ir.analysis.fieldaccess;
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
import static com.android.tools.r8.ir.optimize.info.OptimizationFeedback.getSimpleFeedback;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
import com.android.tools.r8.code.CfOrDexInstanceFieldRead;
import com.android.tools.r8.code.CfOrDexStaticFieldRead;
@@ -53,6 +55,8 @@
private final AppView<AppInfoWithLiveness> appView;
private final PostMethodProcessor.Builder postMethodProcessorBuilder;
+ private final Map<DexEncodedField, ProgramMethodSet> dependencies = new ConcurrentHashMap<>();
+
/** Updated concurrently from {@link #processClass(DexProgramClass)}. */
private final Map<DexEncodedField, AbstractAccessContexts> readFields = new ConcurrentHashMap<>();
@@ -233,6 +237,7 @@
fieldAccessInfoCollection.get(field.getReference()).asMutable().clearReads();
methodsToReprocess.addAll(
contexts.asConcrete().getAccessesWithContexts().values().iterator().next());
+ methodsToReprocess.addAll(dependencies.getOrDefault(field, ProgramMethodSet.empty()));
});
}
@@ -250,6 +255,7 @@
assert !writtenFields.containsKey(field);
methodsToReprocess.addAll(
contexts.asConcrete().getAccessesWithContexts().values().iterator().next());
+ methodsToReprocess.addAll(dependencies.getOrDefault(field, ProgramMethodSet.empty()));
});
}
@@ -326,11 +332,20 @@
return;
}
- if (metadata != null && metadata.isReadForWrite()) {
- // Ignore this read. If the field ends up only being written, then we will still reprocess
- // the method with the read-for-write instruction, since the method contains a write that
- // requires reprocessing.
- return;
+ if (metadata != null) {
+ if (isUnusedReadAfterMethodStaticizing(metadata)) {
+ // Ignore this read.
+ dependencies
+ .computeIfAbsent(field.getDefinition(), ignoreKey(ProgramMethodSet::createConcurrent))
+ .add(getContext());
+ return;
+ }
+ if (metadata.isReadForWrite()) {
+ // Ignore this read. If the field ends up only being written, then we will still reprocess
+ // the method with the read-for-write instruction, since the method contains a write that
+ // requires reprocessing.
+ return;
+ }
}
// Record access.
@@ -352,6 +367,28 @@
}
}
+ private boolean isUnusedReadAfterMethodStaticizing(BytecodeInstructionMetadata metadata) {
+ if (!metadata.isReadForInvokeReceiver()) {
+ return false;
+ }
+ Set<DexMethod> readForInvokeReceiver = metadata.getReadForInvokeReceiver();
+ for (DexMethod methodReference : readForInvokeReceiver) {
+ DexMethod rewrittenMethodReference =
+ appView.graphLens().getRenamedMethodSignature(methodReference, appView.codeLens());
+ DexProgramClass holder =
+ asProgramClassOrNull(appView.definitionFor(rewrittenMethodReference.getHolderType()));
+ ProgramMethod method = rewrittenMethodReference.lookupOnProgramClass(holder);
+ if (method == null) {
+ assert false;
+ return false;
+ }
+ if (!method.getDefinition().isStatic()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private void recordAccessThatCannotBeOptimized(
DexClassAndField field, DexEncodedField definition) {
constantFields.remove(definition);
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldBitAccessInfoTest.java b/src/test/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldBitAccessInfoTest.java
index 82bc605..2e3512d 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldBitAccessInfoTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldBitAccessInfoTest.java
@@ -86,7 +86,7 @@
OptimizationFeedbackMock feedback = new OptimizationFeedbackMock();
FieldBitAccessAnalysis fieldBitAccessAnalysis = new FieldBitAccessAnalysis();
FieldAccessAnalysis fieldAccessAnalysis =
- new FieldAccessAnalysis(appView, null, fieldBitAccessAnalysis, null);
+ new FieldAccessAnalysis(appView, null, fieldBitAccessAnalysis, null, null);
DexProgramClass clazz = appView.appInfo().classes().iterator().next();
assertEquals(TestClass.class.getTypeName(), clazz.type.toSourceString());
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/CompanionConstructorShakingTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/CompanionConstructorShakingTest.java
new file mode 100644
index 0000000..f5fb4f5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/CompanionConstructorShakingTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isStatic;
+import static junit.framework.TestCase.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class CompanionConstructorShakingTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(Main.class)
+ .addOptionsModification(options -> options.enableClassStaticizer = false)
+ .enableInliningAnnotations()
+ .enableNeverClassInliningAnnotations()
+ .enableNoHorizontalClassMergingAnnotations()
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .inspect(
+ inspector -> {
+ ClassSubject hostClassSubject = inspector.clazz(Host.class);
+ assertThat(hostClassSubject, isAbsent());
+
+ ClassSubject companionClassSubject = inspector.clazz(Host.Companion.class);
+ assertThat(companionClassSubject, isPresent());
+ assertEquals(1, companionClassSubject.allMethods().size());
+
+ MethodSubject greetMethodSubject =
+ companionClassSubject.uniqueMethodWithName("greet");
+ assertThat(greetMethodSubject, isStatic());
+ })
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("Hello world!");
+ }
+
+ static class Main {
+
+ public static void main(String[] args) {
+ Host.companion.greet();
+ }
+ }
+
+ static class Host {
+
+ static final Companion companion = new Companion();
+
+ @NeverClassInline
+ @NoHorizontalClassMerging
+ static class Companion {
+
+ @NeverInline
+ void greet() {
+ System.out.println("Hello world!");
+ }
+ }
+ }
+}