Elide ClinitCheck for superclasses with trivial init.
We cannot generally elide ClinitCheck for superclasses
because of escaping instances of erroneous classes and
because a subclass can be successfully initialized while
the superclass is initializing and the subclass remains
initialized even when the superclass initializer throws.
However, for those superclasses that have trivial init,
i.e. that class and its superclasses (and superinterfaces
with default methods) initialize only their own static
fields using constant values, there is no chance to end
up in one of these weird situations and we can safely
eliminate the ClinitCheck.
The size of the aosp_taimen-userdebug prebuilts:
- before:
arm/boot*.oat: 16856928
arm64/boot*.oat: 19846592
oat/arm64/services.odex: 24662880
- after:
arm/boot*.oat: 16848696 (-8.0KiB, -0.05%)
arm64/boot*.oat: 19842320 (-4.2KiB, -0.02%)
oat/arm64/services.odex: 24629952 (-32.2KiB, -0.13%)
with minor changes to other app prebuilts.
Test: Improved 478-checker-clinit-check-pruning.
Test: m test-art-host-gtest
Test: testrunner.py --host --optimizing
Test: Pixel 2 XL boots.
Test: testrunner.py --target --optimizing
Bug: 62478025
Change-Id: I865f567443f1b7f172e5e6559d8eb3232adb7833
diff --git a/compiler/optimizing/instruction_builder.cc b/compiler/optimizing/instruction_builder.cc
index 5c3fcc5..e555d0d 100644
--- a/compiler/optimizing/instruction_builder.cc
+++ b/compiler/optimizing/instruction_builder.cc
@@ -1147,6 +1147,151 @@
MethodCompilationStat::kConstructorFenceGeneratedNew);
}
+static bool IsInBootImage(ObjPtr<mirror::Class> cls, const CompilerOptions& compiler_options)
+ REQUIRES_SHARED(Locks::mutator_lock_) {
+ if (compiler_options.IsBootImage()) {
+ std::string temp;
+ const char* descriptor = cls->GetDescriptor(&temp);
+ return compiler_options.IsImageClass(descriptor);
+ } else {
+ return Runtime::Current()->GetHeap()->FindSpaceFromObject(cls, false)->IsImageSpace();
+ }
+}
+
+static bool IsSubClass(ObjPtr<mirror::Class> to_test, ObjPtr<mirror::Class> super_class)
+ REQUIRES_SHARED(Locks::mutator_lock_) {
+ return to_test != nullptr && !to_test->IsInterface() && to_test->IsSubClass(super_class);
+}
+
+static bool HasTrivialClinit(ObjPtr<mirror::Class> klass, PointerSize pointer_size)
+ REQUIRES_SHARED(Locks::mutator_lock_) {
+ // Check if the class has encoded fields that trigger bytecode execution.
+ // (Encoded fields are just a different representation of <clinit>.)
+ if (klass->NumStaticFields() != 0u) {
+ DCHECK(klass->GetClassDef() != nullptr);
+ EncodedStaticFieldValueIterator it(klass->GetDexFile(), *klass->GetClassDef());
+ for (; it.HasNext(); it.Next()) {
+ switch (it.GetValueType()) {
+ case EncodedArrayValueIterator::ValueType::kBoolean:
+ case EncodedArrayValueIterator::ValueType::kByte:
+ case EncodedArrayValueIterator::ValueType::kShort:
+ case EncodedArrayValueIterator::ValueType::kChar:
+ case EncodedArrayValueIterator::ValueType::kInt:
+ case EncodedArrayValueIterator::ValueType::kLong:
+ case EncodedArrayValueIterator::ValueType::kFloat:
+ case EncodedArrayValueIterator::ValueType::kDouble:
+ case EncodedArrayValueIterator::ValueType::kNull:
+ case EncodedArrayValueIterator::ValueType::kString:
+ // Primitive, null or j.l.String initialization is permitted.
+ break;
+ case EncodedArrayValueIterator::ValueType::kType:
+ // Type initialization can load classes and execute bytecode through a class loader
+ // which can execute arbitrary bytecode. We do not optimize for known class loaders;
+ // kType is rarely used (if ever).
+ return false;
+ default:
+ // Other types in the encoded static field list are rejected by the DexFileVerifier.
+ LOG(FATAL) << "Unexpected type " << it.GetValueType();
+ UNREACHABLE();
+ }
+ }
+ }
+ // Check if the class has <clinit> that executes arbitrary code.
+ // Initialization of static fields of the class itself with constants is allowed.
+ ArtMethod* clinit = klass->FindClassInitializer(pointer_size);
+ if (clinit != nullptr) {
+ const DexFile& dex_file = *clinit->GetDexFile();
+ CodeItemInstructionAccessor accessor(dex_file, clinit->GetCodeItem());
+ for (DexInstructionPcPair it : accessor) {
+ switch (it->Opcode()) {
+ case Instruction::CONST_4:
+ case Instruction::CONST_16:
+ case Instruction::CONST:
+ case Instruction::CONST_HIGH16:
+ case Instruction::CONST_WIDE_16:
+ case Instruction::CONST_WIDE_32:
+ case Instruction::CONST_WIDE:
+ case Instruction::CONST_WIDE_HIGH16:
+ case Instruction::CONST_STRING:
+ case Instruction::CONST_STRING_JUMBO:
+ // Primitive, null or j.l.String initialization is permitted.
+ break;
+ case Instruction::RETURN_VOID:
+ case Instruction::RETURN_VOID_NO_BARRIER:
+ break;
+ case Instruction::SPUT:
+ case Instruction::SPUT_WIDE:
+ case Instruction::SPUT_OBJECT:
+ case Instruction::SPUT_BOOLEAN:
+ case Instruction::SPUT_BYTE:
+ case Instruction::SPUT_CHAR:
+ case Instruction::SPUT_SHORT:
+ // Only initialization of a static field of the same class is permitted.
+ if (dex_file.GetFieldId(it->VRegB_21c()).class_idx_ != klass->GetDexTypeIndex()) {
+ return false;
+ }
+ break;
+ case Instruction::NEW_ARRAY:
+ // Only primitive arrays are permitted.
+ if (Primitive::GetType(dex_file.GetTypeDescriptor(dex_file.GetTypeId(
+ dex::TypeIndex(it->VRegC_22c())))[1]) == Primitive::kPrimNot) {
+ return false;
+ }
+ break;
+ case Instruction::APUT:
+ case Instruction::APUT_WIDE:
+ case Instruction::APUT_BOOLEAN:
+ case Instruction::APUT_BYTE:
+ case Instruction::APUT_CHAR:
+ case Instruction::APUT_SHORT:
+ case Instruction::FILL_ARRAY_DATA:
+ case Instruction::NOP:
+ // Allow initialization of primitive arrays (only constants can be stored).
+ // Note: We expect NOPs used for fill-array-data-payload but accept all NOPs
+ // (even unreferenced switch payloads if they make it through the verifier).
+ break;
+ default:
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+static bool HasTrivialInitialization(ObjPtr<mirror::Class> cls,
+ const CompilerOptions& compiler_options)
+ REQUIRES_SHARED(Locks::mutator_lock_) {
+ Runtime* runtime = Runtime::Current();
+ PointerSize pointer_size = runtime->GetClassLinker()->GetImagePointerSize();
+
+ // Check the superclass chain.
+ for (ObjPtr<mirror::Class> klass = cls; klass != nullptr; klass = klass->GetSuperClass()) {
+ if (klass->IsInitialized() && IsInBootImage(klass, compiler_options)) {
+ break; // `klass` and its superclasses are already initialized in the boot image.
+ }
+ if (!HasTrivialClinit(klass, pointer_size)) {
+ return false;
+ }
+ }
+
+ // Also check interfaces with default methods as they need to be initialized as well.
+ ObjPtr<mirror::IfTable> iftable = cls->GetIfTable();
+ DCHECK(iftable != nullptr);
+ for (int32_t i = 0, count = iftable->Count(); i != count; ++i) {
+ ObjPtr<mirror::Class> iface = iftable->GetInterface(i);
+ if (!iface->HasDefaultMethods()) {
+ continue; // Initializing `cls` does not initialize this interface.
+ }
+ if (iface->IsInitialized() && IsInBootImage(iface, compiler_options)) {
+ continue; // This interface is already initialized in the boot image.
+ }
+ if (!HasTrivialClinit(iface, pointer_size)) {
+ return false;
+ }
+ }
+ return true;
+}
+
bool HInstructionBuilder::IsInitialized(ScopedObjectAccess& soa, Handle<mirror::Class> cls) const {
if (cls == nullptr) {
return false;
@@ -1162,35 +1307,60 @@
}
// Assume loaded only if klass is in the boot image. App classes cannot be assumed
// loaded because we don't even know what class loader will be used to load them.
- const CompilerOptions& compiler_options = compiler_driver_->GetCompilerOptions();
- if (compiler_options.IsBootImage()) {
- std::string temp;
- const char* descriptor = cls->GetDescriptor(&temp);
- if (compiler_options.IsImageClass(descriptor)) {
- return true;
- }
- } else {
- if (runtime->GetHeap()->FindSpaceFromObject(cls.Get(), false)->IsImageSpace()) {
- return true;
- }
+ if (IsInBootImage(cls.Get(), compiler_driver_->GetCompilerOptions())) {
+ return true;
}
}
- // We can avoid the class initialization check for `cls` only in static methods in the
+ // We can avoid the class initialization check for `cls` in static methods in the
// very same class. Instance methods of the same class can run on an escaped instance
// of an erroneous class. Even a superclass may need to be checked as the subclass
// can be completely initialized while the superclass is initializing and the subclass
// remains initialized when the superclass initializer throws afterwards. b/62478025
// Note: The HClinitCheck+HInvokeStaticOrDirect merging can still apply.
- if ((dex_compilation_unit_->GetAccessFlags() & kAccStatic) != 0u) {
- ObjPtr<mirror::Class> outermost_cls = ResolveOutermostCompilingClass(soa);
- if (outermost_cls == cls.Get()) {
+ ObjPtr<mirror::Class> outermost_cls = ResolveOutermostCompilingClass(soa);
+ bool is_static = (dex_compilation_unit_->GetAccessFlags() & kAccStatic) != 0u;
+ if (is_static && outermost_cls == cls.Get()) {
+ return true;
+ }
+ // Remember if the compiled class is a subclass of `cls`. By the time this is used
+ // below the `outermost_cls` may be invalidated by calling ResolveCompilingClass().
+ bool is_subclass = IsSubClass(outermost_cls, cls.Get());
+ if (dex_compilation_unit_ != outer_compilation_unit_) {
+ // Check also the innermost method. Though excessive copies of ClinitCheck can be
+ // eliminated by GVN, that happens only after the decision whether to inline the
+ // graph or not and that may depend on the presence of the ClinitCheck.
+ // TODO: We should walk over the entire inlined method chain, but we don't pass that
+ // information to the builder.
+ ObjPtr<mirror::Class> innermost_cls = ResolveCompilingClass(soa);
+ if (is_static && innermost_cls == cls.Get()) {
return true;
}
+ is_subclass = is_subclass || IsSubClass(innermost_cls, cls.Get());
}
- // Note: We could walk over the inlined methods to avoid allocating excessive
- // `HClinitCheck`s in inlined static methods but they shall be eliminated by GVN.
+ // Otherwise, we may be able to avoid the check if `cls` is a superclass of a method being
+ // compiled here (anywhere in the inlining chain) as the `cls` must have started initializing
+ // before calling any `cls` or subclass methods. Static methods require a clinit check and
+ // instance methods require an instance which cannot be created before doing a clinit check.
+ // When a subclass of `cls` starts initializing, it starts initializing its superclass
+ // chain up to `cls` without running any bytecode, i.e. without any opportunity for circular
+ // initialization weirdness.
+ //
+ // If the initialization of `cls` is trivial (`cls` and its superclasses and superinterfaces
+ // with default methods initialize only their own static fields using constant values), it must
+ // complete, either successfully or by throwing and marking `cls` erroneous, without allocating
+ // any instances of `cls` or subclasses (or any other class) and without calling any methods.
+ // If it completes by throwing, no instances of `cls` shall be created and no subclass method
+ // bytecode shall execute (see above), therefore the instruction we're building shall be
+ // unreachable. By reaching the instruction, we know that `cls` was initialized successfully.
+ //
+ // TODO: We should walk over the entire inlined methods chain, but we don't pass that
+ // information to the builder. (We could also check if we're guaranteed a non-null instance
+ // of `cls` at this location but that's outside the scope of the instruction builder.)
+ if (is_subclass && HasTrivialInitialization(cls.Get(), compiler_driver_->GetCompilerOptions())) {
+ return true;
+ }
return false;
}