Require instance fields are kept when instantiating classes using Unsafe
This effectively reverts 6250eeeafa351b13ef371e8a67f5b8c641459e1b.
The reason for the revert is that this causes +1% regressions in dex size. When sun.misc.Unsafe is used to instantiate a class without an instance initializer, the previous behavior can be restored by keeping the instance fields of the instantiated class. See also DefaultFieldValueAnalysisWithKeptSubclassTest.
Bug: b/379034741
Change-Id: Ie3795040413e888691ae1316e1878da9f77f6707
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
index 4c9489a..bd2fdb3 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
@@ -56,6 +56,9 @@
public class DefaultFieldValueJoiner {
+ private static final boolean includeDefaultValueForAllFieldsWhereHolderIsKeptOrHasKeptSubclass =
+ false;
+
private final AppView<AppInfoWithLiveness> appView;
private final Set<DexProgramClass> classesWithSingleCallerInlinedInstanceInitializers;
private final FieldStateCollection fieldStates;
@@ -88,26 +91,28 @@
// Classes that are kept or have a kept subclass can be instantiated in a way that does not use
// any instance initializers. For all fields on such classes, we therefore include the default
// field value.
- Map<DexProgramClass, Boolean> classesWithKeptSubclasses =
- computeClassesWithKeptSubclasses(fieldsOfInterest);
ProgramFieldSet fieldsWithLiveDefaultValue = ProgramFieldSet.createConcurrent();
- MapUtils.removeIf(
- fieldsOfInterest,
- (clazz, fields) -> {
- Boolean isKeptOrHasKeptSubclass = classesWithKeptSubclasses.get(clazz);
- assert isKeptOrHasKeptSubclass != null;
- if (isKeptOrHasKeptSubclass) {
- fields.removeIf(
- field -> {
- if (field.getDefinition().isInstance()) {
- fieldsWithLiveDefaultValue.add(field);
- return true;
- }
- return false;
- });
- }
- return fields.isEmpty();
- });
+ if (includeDefaultValueForAllFieldsWhereHolderIsKeptOrHasKeptSubclass) {
+ Map<DexProgramClass, Boolean> classesWithKeptSubclasses =
+ computeClassesWithKeptSubclasses(fieldsOfInterest);
+ MapUtils.removeIf(
+ fieldsOfInterest,
+ (clazz, fields) -> {
+ Boolean isKeptOrHasKeptSubclass = classesWithKeptSubclasses.get(clazz);
+ assert isKeptOrHasKeptSubclass != null;
+ if (isKeptOrHasKeptSubclass) {
+ fields.removeIf(
+ field -> {
+ if (field.getDefinition().isInstance()) {
+ fieldsWithLiveDefaultValue.add(field);
+ return true;
+ }
+ return false;
+ });
+ }
+ return fields.isEmpty();
+ });
+ }
// If constructor inlining is disabled, then we focus on whether each instance initializer
// definitely assigns the given field before it is read. We do the same for final and static
@@ -330,6 +335,12 @@
(holderType, fields) -> {
assert !fields.isEmpty();
DexProgramClass holder = fields.iterator().next().getHolder();
+ // If the class is kept it could be instantiated directly, in which case all default field
+ // values could be live.
+ if (appView.getKeepInfo(holder).isPinned(appView.options())) {
+ fields.forEach(liveDefaultValueConsumer);
+ return true;
+ }
if (holder.isFinal() || !appView.appInfo().isInstantiatedIndirectly(holder)) {
// When the class is not explicitly marked final, the class could in principle have
// injected subclasses if it is pinned. However, none of the fields are pinned, so we
diff --git a/src/test/examplesJava17/records/RecordBlogTest.java b/src/test/examplesJava17/records/RecordBlogTest.java
index c28b92b..1128f07 100644
--- a/src/test/examplesJava17/records/RecordBlogTest.java
+++ b/src/test/examplesJava17/records/RecordBlogTest.java
@@ -33,7 +33,7 @@
ImmutableMap.<String, String>builder()
.put("-dontobfuscate\n-dontoptimize", "RecordBlog$Person[name=%s, age=42]")
.put("", "a[a=%s]")
- .put("-keep,allowshrinking class " + CLASS, "RecordBlog$Person[a=%s, b=42]")
+ .put("-keep,allowshrinking class " + CLASS, "RecordBlog$Person[a=%s]")
.put(
"-keepclassmembers,allowshrinking,allowoptimization class "
+ CLASS
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/DefaultFieldValueAnalysisWithKeptSubclassTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/DefaultFieldValueAnalysisWithKeptSubclassTest.java
index dd9a582..9987908 100644
--- a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/DefaultFieldValueAnalysisWithKeptSubclassTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/DefaultFieldValueAnalysisWithKeptSubclassTest.java
@@ -4,6 +4,7 @@
package com.android.tools.r8.optimize.argumentpropagation;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
import static org.hamcrest.MatcherAssert.assertThat;
import com.android.tools.r8.NeverClassInline;
@@ -11,10 +12,12 @@
import com.android.tools.r8.NoVerticalClassMerging;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRunResult;
import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.utils.BooleanUtils;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import java.lang.reflect.Field;
+import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -26,15 +29,20 @@
public class DefaultFieldValueAnalysisWithKeptSubclassTest extends TestBase {
@Parameter(0)
+ public boolean keepFields;
+
+ @Parameter(1)
public TestParameters parameters;
@Parameters(name = "{0}")
- public static TestParametersCollection data() {
- return getTestParameters()
- .withCfRuntimes()
- .withDexRuntimesStartingFromIncluding(Version.V5_1_1)
- .withAllApiLevels()
- .build();
+ public static List<Object[]> data() {
+ return buildParameters(
+ BooleanUtils.values(),
+ getTestParameters()
+ .withCfRuntimes()
+ .withDexRuntimesStartingFromIncluding(Version.V5_1_1)
+ .withAllApiLevels()
+ .build());
}
@Test
@@ -55,6 +63,13 @@
.addInnerClasses(getClass())
.addKeepClassAndMembersRules(Main.class)
.addKeepClassRules(B.class)
+ .applyIf(
+ keepFields,
+ b ->
+ b.addKeepRules(
+ "-keepclassmembers class " + A.class.getTypeName() + " {",
+ " !static <fields>;",
+ "}"))
.enableInliningAnnotations()
.enableNeverClassInliningAnnotations()
.enableNoVerticalClassMergingAnnotations()
@@ -64,10 +79,13 @@
inspector -> {
ClassSubject aClassSubject = inspector.clazz(A.class);
assertThat(aClassSubject, isPresent());
- assertThat(aClassSubject.uniqueFieldWithOriginalName("f"), isPresent());
+ assertThat(aClassSubject.uniqueFieldWithOriginalName("f"), isPresentIf(keepFields));
})
.run(parameters.getRuntime(), Main.class, B.class.getTypeName())
- .assertSuccessWithOutputLines("Hello, world!");
+ .applyIf(
+ keepFields,
+ rr -> rr.assertSuccessWithOutputLines("Hello, world!"),
+ TestRunResult::assertSuccessWithEmptyOutput);
}
static class Main {
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedFieldTypeTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedFieldTypeTest.java
index 181a85a..693a077 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedFieldTypeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedFieldTypeTest.java
@@ -118,7 +118,7 @@
ClassSubject testClassSubject = inspector.clazz(TestClass.class);
assertThat(testClassSubject, isPresent());
- if (enableVerticalClassMerging) {
+ if (enableVerticalClassMerging && parameters.canInitNewInstanceUsingSuperclassConstructor()) {
// Verify that TestClass.field has been removed.
assertEquals(1, testClassSubject.allFields().size());