opt/kotlin-lambda/KotlinObjectInliner.cpp (499 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "KotlinObjectInliner.h"
#include "CFGMutation.h"
#include "ConcurrentContainers.h"
#include "Creators.h"
#include "IRCode.h"
#include "LiveRange.h"
#include "Mutators.h"
#include "PassManager.h"
#include "ScopedCFG.h"
#include "Show.h"
#include "Walkers.h"
namespace {
void dump_cls(DexClass* cls) {
if (traceEnabled(KOTLIN_OBJ_INLINE, 5)) {
TRACE(KOTLIN_OBJ_INLINE, 5, "Class %s", SHOW(cls));
std::vector<DexMethod*> methods = cls->get_all_methods();
std::vector<DexField*> fields = cls->get_all_fields();
for (auto* v : fields) {
TRACE(KOTLIN_OBJ_INLINE, 5, "Field %s", SHOW(v));
}
for (auto* v : methods) {
TRACE(KOTLIN_OBJ_INLINE, 5, "Method %s", SHOW(v));
if (v->get_code()) {
TRACE(KOTLIN_OBJ_INLINE, 5, "%s", SHOW(v->get_code()));
}
}
}
}
// check if CLS is an inner class and return the outer class. Return nullptr if
// this is not an inner class.
DexClass* get_outer_class(const DexClass* cls) {
const std::string& cls_name = cls->get_name()->str();
auto cash_idx = cls_name.find_last_of('$');
if (cash_idx == std::string::npos) {
// this is not an inner class
return nullptr;
}
auto slash_idx = cls_name.find_last_of('/');
if (slash_idx == std::string::npos || slash_idx < cash_idx) {
// there's a $ in the class name
const std::string& outer_name = cls_name.substr(0, cash_idx) + ';';
DexType* outer = DexType::get_type(outer_name);
if (outer == nullptr) {
return nullptr;
}
DexClass* outer_cls = type_class(outer);
if (outer_cls == nullptr || outer_cls->is_external()) {
return nullptr;
}
return outer_cls;
}
return nullptr;
}
// Check if the method uses the first argument (or this pointer).
// if strict == true, any use of this_reg will result in returning true.
// if strict == false, if his_reg is used just to invoke virtual
// methods from the same class, this will not be considered a use.
bool uses_this(const DexMethod* method, bool strict = false) {
auto code = method->get_code();
auto iterable = InstructionIterable(code);
auto it = iterable.begin();
auto const this_load_insn = it->insn;
if (this_load_insn->opcode() != IOPCODE_LOAD_PARAM_OBJECT) {
return false;
}
std::unordered_set<reg_t> this_reg_set;
auto const this_reg = this_load_insn->dest();
this_reg_set.insert(this_reg);
for (const auto& mie : iterable) {
auto insn = mie.insn;
for (unsigned i = 0; i < insn->srcs_size(); i++) {
if (this_reg_set.count(insn->src(i))) {
if (!strict && i == 0 && insn->opcode() == OPCODE_INVOKE_VIRTUAL &&
insn->get_method()->get_class() == method->get_class()) {
continue;
}
return true;
}
}
}
return false;
}
// Make method static (if necessary) and relocate to TO_TYPE
void make_static_and_relocate_method(DexMethod* method, DexType* to_type) {
if (!is_static(method)) {
mutators::make_static(method,
uses_this(method, true) ? mutators::KeepThis::Yes
: mutators::KeepThis::No);
}
relocate_method(method, to_type);
}
// Check if CLS is a companion object
// Companion object is:
// 1. Inner Object class:
// 2. Will not have <clinit>
// 3. Will not have any direct methods other than constructors
// 4. Will not have any fields (TODO We could extend to support sfields)
// 5. Outer (or parent) class may have <clinit> which create instance of this
// (parent has sfield of inner class)
// 6. CLS is final and extends J_L_O
// If this is a candidate, return outer class. Return nullptr otherwise.
DexClass* candidate_for_companion_inlining(DexClass* cls) {
if (!is_final(cls) || !cls->get_ifields().empty() ||
!cls->get_interfaces()->empty() || cls->get_clinit() ||
!cls->get_sfields().empty() ||
cls->get_super_class() != type::java_lang_Object()) {
if (boost::ends_with(cls->get_name()->str(), "$Companion;")) {
TRACE(KOTLIN_OBJ_INLINE, 5, "Rejected $Companion cls = %s", SHOW(cls));
}
return nullptr;
}
DexClass* outer_cls = get_outer_class(cls);
if (!outer_cls || is_abstract(outer_cls)) {
return nullptr;
}
bool found = false;
for (auto* sfield : outer_cls->get_sfields()) {
if (sfield->get_type() == cls->get_type()) {
if (found) {
// Expect only one sfield in outer class to hold companion object
// instance
TRACE(KOTLIN_OBJ_INLINE, 5, "3 Rejected cls = %s", SHOW(cls));
return nullptr;
}
found = true;
}
}
for (auto meth : cls->get_vmethods()) {
if (meth->rstate.no_optimizations() || !is_final(meth) ||
!meth->get_code() || uses_this(meth)) {
TRACE(KOTLIN_OBJ_INLINE, 5, "Failed due to method = %s", SHOW(meth));
return nullptr;
}
}
for (auto meth : cls->get_dmethods()) {
if (method::is_init(meth) || method::is_clinit(meth)) {
continue;
}
if (meth->rstate.no_optimizations() || !meth->get_code() ||
uses_this(meth)) {
TRACE(KOTLIN_OBJ_INLINE, 5, "Failed due to method = %s", SHOW(meth));
return nullptr;
}
}
return outer_cls;
}
void relocate(DexClass* from,
DexClass* to,
std::unordered_set<DexMethodRef*>& relocated_methods) {
// Remove the instance in TO class
DexField* field = nullptr;
for (auto* sfield : to->get_sfields()) {
if (type_class(sfield->get_type()) == from) {
always_assert(field == nullptr);
field = sfield;
}
}
TRACE(KOTLIN_OBJ_INLINE, 5, "Relocating from:");
dump_cls(from);
TRACE(KOTLIN_OBJ_INLINE, 5, "Relocating to:");
dump_cls(to);
// Remove the <init> in the <clinit>
if (to->get_clinit()) {
auto* clinit_method = to->get_clinit();
auto code = clinit_method->get_code();
cfg::ScopedCFG clinit_cfg(code);
cfg::CFGMutation m(*clinit_cfg);
auto iterable = cfg::InstructionIterable(*clinit_cfg);
for (auto it = iterable.begin(); it != iterable.end(); it++) {
auto insn = it->insn;
if (opcode::is_new_instance(insn->opcode())) {
auto* host_typ = insn->get_type();
if (host_typ == from->get_type()) {
auto mov_result_it = clinit_cfg->move_result_of(it);
auto init_null = new IRInstruction(OPCODE_CONST);
init_null->set_literal(0);
init_null->set_dest(mov_result_it->insn->dest());
m.replace(it, {init_null});
TRACE(KOTLIN_OBJ_INLINE, 5, "Remove insn %s", SHOW(insn));
}
}
if (opcode::is_an_invoke(insn->opcode()) &&
method::is_init(insn->get_method())) {
auto* host_typ = insn->get_method()->get_class();
if (host_typ == from->get_type()) {
m.remove(it);
TRACE(KOTLIN_OBJ_INLINE, 5, "Remove insn %s", SHOW(insn));
}
}
if (opcode::is_an_sput(insn->opcode()) && insn->get_field() == field) {
TRACE(KOTLIN_OBJ_INLINE, 5, "Remove insn %s", SHOW(insn));
m.remove(it);
}
}
m.flush();
}
if (field) {
TRACE(KOTLIN_OBJ_INLINE, 5, "Remove field %s", SHOW(field));
to->remove_field(field);
}
// Relocate the methods from FROM to TO
for (auto* method : from->get_vmethods()) {
TRACE(KOTLIN_OBJ_INLINE,
5,
"Relocating :(%s) %s -> %s",
SHOW(method),
SHOW(from),
SHOW(to));
make_static_and_relocate_method(method, to->get_type());
relocated_methods.insert(method);
}
for (auto* method : from->get_dmethods()) {
if (method::is_init(method) || method::is_clinit(method)) {
continue;
}
TRACE(KOTLIN_OBJ_INLINE,
5,
"Relocating static method:(%s from) %s -> %s",
SHOW(from),
SHOW(method),
SHOW(to));
make_static_and_relocate_method(method, to->get_type());
relocated_methods.insert(method);
}
for (auto* f : from->get_sfields()) {
TRACE(KOTLIN_OBJ_INLINE,
5,
"Relocating static field:(%s from) %s -> %s",
SHOW(from),
SHOW(field),
SHOW(to));
relocate_field(f, to->get_type());
}
TRACE(KOTLIN_OBJ_INLINE, 5, "After relocating to:");
dump_cls(to);
}
bool is_def_tractable(IRInstruction* insn,
const DexClass* from,
live_range::MoveAwareChains& move_aware_chains) {
auto du_chains_move_aware = move_aware_chains.get_def_use_chains();
if (!du_chains_move_aware.count(insn)) {
// No use insns.
return true;
}
const auto& use_set = du_chains_move_aware.at(insn);
for (const auto& p : use_set) {
auto use_insn = p.insn;
auto use_index = p.src_index;
switch (use_insn->opcode()) {
case OPCODE_MOVE_OBJECT:
break;
case OPCODE_INVOKE_STATIC: {
// JVM static
if (use_index != 0 ||
type_class(use_insn->get_method()->get_class()) != from) {
TRACE(KOTLIN_OBJ_INLINE,
2,
"Adding cls %s to bad list due to insn %s",
SHOW(from),
SHOW(use_insn));
return false;
}
} break;
case OPCODE_INVOKE_VIRTUAL:
case OPCODE_INVOKE_INTERFACE:
// Check for likes of Ljava/lang/Object;.getClass:()Ljava/lang/Class;
if (use_insn->get_method()->get_class() != from->get_type() ||
use_index != 0) {
TRACE(KOTLIN_OBJ_INLINE,
2,
"Adding cls %s to bad list due to insn %s",
SHOW(from),
SHOW(use_insn));
return false;
}
break;
default:
TRACE(KOTLIN_OBJ_INLINE,
2,
"Adding cls %s to bad list due to insn %s",
SHOW(from),
SHOW(use_insn));
return false;
}
}
return true;
}
} // namespace
void KotlinObjectInliner::run_pass(DexStoresVector& stores,
ConfigFiles&,
PassManager& mgr) {
const auto scope = build_class_scope(stores);
ConcurrentMap<DexClass*, DexClass*> map;
ConcurrentSet<DexClass*> bad;
std::unordered_map<DexClass*, unsigned> outer_cls_count;
std::unordered_set<DexType*> do_not_inline_set;
Stats stats;
for (auto& p : m_do_not_inline_list) {
auto* do_not_inline_cls = DexType::get_type(p);
if (do_not_inline_cls) {
TRACE(KOTLIN_OBJ_INLINE,
2,
"do_not_inlin_cls : %s",
SHOW(do_not_inline_cls));
do_not_inline_set.insert(do_not_inline_cls);
}
}
// Collect candidates
walk::parallel::classes(scope, [&](DexClass* cls) {
if (is_native(cls) || root(cls) || !can_rename(cls) || !can_delete(cls) ||
cls->rstate.is_referenced_by_resource_xml() || cls->is_external() ||
do_not_inline_set.count(cls->get_type())) {
return;
}
auto outer_cls = candidate_for_companion_inlining(cls);
if (outer_cls && !outer_cls->rstate.is_referenced_by_resource_xml() &&
!do_not_inline_set.count(outer_cls->get_type())) {
// This is a candidate for inlining
map.insert(std::make_pair(cls, outer_cls));
TRACE(KOTLIN_OBJ_INLINE, 2, "Candidate cls : %s", SHOW(cls));
}
});
stats.kotlin_candidate_companion_objects = map.size();
for (auto& iter : map) {
outer_cls_count[iter.second]++;
}
for (auto iter : map) {
// We have mutiple companion objects.
if (outer_cls_count.find(iter.second)->second != 1) {
bad.insert(iter.first);
}
}
// Filter out any instance whose use is not tractable
walk::parallel::methods(scope, [&](DexMethod* method) {
auto code = method->get_code();
if (!code) {
return;
}
// we cannot relocate returning companion obect.
auto* rtype = type_class(method->get_proto()->get_rtype());
if (rtype && map.count(rtype)) {
bad.insert(rtype);
}
cfg::ScopedCFG cfg(code);
auto iterable = cfg::InstructionIterable(*cfg);
live_range::MoveAwareChains move_aware_chains(*cfg);
for (auto it = iterable.begin(); it != iterable.end(); it++) {
auto insn = it->insn;
switch (insn->opcode()) {
case OPCODE_SPUT_OBJECT: {
auto* from = type_class(insn->get_field()->get_type());
if (!from || !map.count(from) || bad.count(from)) {
break;
}
// Shold only be set from parent's <clinit>
// Otherwise add it to bad list.
if (method::is_clinit(method) &&
type_class(method->get_class()) == map.find(from)->second) {
break;
}
bad.insert(from);
break;
}
// If there is any instance field, add it to bad
case OPCODE_IPUT_OBJECT:
case OPCODE_IGET_OBJECT: {
auto* from = type_class(insn->get_field()->get_type());
if (!from || !map.count(from) || bad.count(from)) {
break;
}
bad.insert(from);
break;
}
case OPCODE_SGET_OBJECT: {
auto* from = type_class(insn->get_field()->get_type());
if (!from || !map.count(from) || bad.count(from)) {
break;
}
// Check we can track the uses of the Companion object instance.
// i.e. Companion object is only used to invoke methods
if (!is_def_tractable(insn, from, move_aware_chains)) {
bad.insert(from);
}
break;
}
case OPCODE_INSTANCE_OF:
case OPCODE_NEW_INSTANCE: {
auto* from = type_class(insn->get_type());
if (!from || !map.count(from) || bad.count(from)) {
break;
}
if (method::is_clinit(method) &&
type_class(method->get_class()) == map.find(from)->second) {
break;
}
bad.insert(from);
TRACE(KOTLIN_OBJ_INLINE,
2,
"Adding cls %s to bad list due to insn %s",
SHOW(from),
SHOW(insn));
break;
}
case OPCODE_CHECK_CAST: {
auto* from = type_class(insn->get_type());
if (!from || !map.count(from) || bad.count(from)) {
break;
}
bad.insert(from);
TRACE(KOTLIN_OBJ_INLINE,
2,
"Adding cls %s to bad list due to insn %s",
SHOW(from),
SHOW(insn));
break;
}
case OPCODE_INVOKE_DIRECT: {
auto* from = type_class(insn->get_method()->get_class());
if (!method::is_init(insn->get_method()) || !from || !map.count(from) ||
bad.count(from)) {
break;
}
if ((type_class(method->get_class()) == from &&
method::is_init(method)) ||
((type_class(method->get_class()) == map.find(from)->second) &&
method::is_clinit(method))) {
break;
}
bad.insert(from);
break;
}
default:
if (insn->has_type()) {
auto* from = type_class(insn->get_type());
if (!from || !map.count(from) || bad.count(from)) {
break;
}
bad.insert(from);
TRACE(KOTLIN_OBJ_INLINE,
2,
"Adding cls %s to bad list due to insn %s",
SHOW(from),
SHOW(insn));
break;
}
break;
}
}
});
stats.kotlin_untrackable_companion_objects = bad.size();
// Inline objects in candidate to maped class
//
std::unordered_set<DexMethodRef*> relocated_methods;
for (auto& p : map) {
auto* from_cls = p.first;
auto* to_cls = p.second;
if (!bad.count(from_cls)) {
TRACE(KOTLIN_OBJ_INLINE,
2,
"Relocate : %s -> %s",
SHOW(from_cls),
SHOW(to_cls));
relocate(from_cls, to_cls, relocated_methods);
stats.kotlin_companion_objects_inlined++;
}
}
// Fix virtual call arguments
walk::parallel::methods(scope, [&](DexMethod* method) {
auto code = method->get_code();
if (code == nullptr) {
return;
}
bool changed = false;
cfg::ScopedCFG cfg(method->get_code());
cfg::CFGMutation m(*cfg);
live_range::MoveAwareChains move_aware_chains(*cfg);
auto du_chains_move_aware = move_aware_chains.get_def_use_chains();
auto iterable = cfg::InstructionIterable(*cfg);
for (auto it = iterable.begin(); it != iterable.end(); it++) {
auto insn = it->insn;
if (opcode::is_an_sput(insn->opcode())) {
auto* from = type_class(insn->get_field()->get_type());
if (!from || !map.count(from) || bad.count(from)) {
continue;
}
auto mov_result_it = cfg->move_result_of(it);
auto init_null = new IRInstruction(OPCODE_CONST);
init_null->set_literal(0);
init_null->set_dest(mov_result_it->insn->dest());
m.replace(it, {init_null});
changed = true;
}
if (insn->opcode() == OPCODE_INVOKE_VIRTUAL) {
if (!relocated_methods.count(insn->get_method())) {
continue;
}
insn->set_opcode(OPCODE_INVOKE_STATIC);
size_t arg_count = insn->get_method()->get_proto()->get_args()->size();
auto nargs = insn->srcs_size();
if (arg_count != nargs) {
for (uint16_t i = 0; i < nargs - 1; i++) {
insn->set_src(i, insn->src(i + 1));
}
insn->set_srcs_size(nargs - 1);
}
always_assert(arg_count == insn->srcs_size());
changed = true;
}
}
if (changed) {
m.flush();
TRACE(KOTLIN_OBJ_INLINE, 5, "After : %s\n", SHOW(method));
TRACE(KOTLIN_OBJ_INLINE, 5, "%s\n", SHOW(*cfg));
}
});
stats.report(mgr);
}
void KotlinObjectInliner::Stats::report(PassManager& mgr) const {
mgr.incr_metric("kotlin_candidate_companion_objects",
kotlin_candidate_companion_objects);
mgr.incr_metric("kotlin_untrackable_companion_objects",
kotlin_untrackable_companion_objects);
mgr.incr_metric("kotlin_companion_objects_inlined",
kotlin_companion_objects_inlined);
TRACE(KOTLIN_OBJ_INLINE, 2, "KotlinObjectInliner Stats:");
TRACE(KOTLIN_OBJ_INLINE,
2,
"kotlin_candidate_companion_objects = %lu",
kotlin_candidate_companion_objects);
TRACE(KOTLIN_OBJ_INLINE,
2,
"kotlin_untrackable_companion_objects = %lu",
kotlin_untrackable_companion_objects);
TRACE(KOTLIN_OBJ_INLINE,
2,
"kotlin_companion_objects_inlined = %lu",
kotlin_companion_objects_inlined);
}
static KotlinObjectInliner s_pass;