Introduction
When Kotlin compile a class with lambda inline to JVM bytecode, kotlin compiler backend will generate a INSTANCE field in generated bytecode. Such as :
import java.util.*
import kotlinx.coroutines.*
class KotlinLambdaInline {
var sink: Long = 0
fun doCalc(addfn: (a: Long, b: Long) -> Long): Long {
return addfn(123L, 456L)
}
fun foo() {
sink = doCalc { a: Long, b: Long -> a + b }
}
}
Will generate bytecode like : https://gist.github.com/lfkdsk/41fe56a59365ad73cf430c6ef94e91df
RewriteKotlinSingletonInstancePass will remove these singleton instance in some cases.
Optimization
Optimize Kotlin non-capturing Lambda · facebook/redex@1da6807
This pass will remove INSTANCE in following several conditions.
- Single use for the instance field
- Anonymous class is final
- Anonymous class has single
Step 1: Collect Kotlin Lambda INSTANCE Usage
Collect Kotlin noncapturing Lambda which would have an INSTANCE field of the same type and is initialized in
walk::parallel::classes(scope, [&](DexClass* cls) {
if (!can_rename(cls) || !can_delete(cls)) {
return;
}
// has instance field just check field name == 'INSTANCE'
auto instance = has_instance_field(cls, m_instance);
if (!instance) {
return;
}
// another closure check cls has side effects.
if (do_not_consider_type(cls)) {
return;
}
if (concurrent_instance_map.count(instance)) {
return;
}
std::set<std::pair<IRInstruction*, DexMethod*>> insns;
concurrent_instance_map.emplace(instance, insns);
});
Check side effects:
auto iterable = cfg::InstructionIterable(*cfg);
side_effects::Summary summary(side_effects::EFF_NONE, {});
side_effects::InvokeToSummaryMap summary_map;
for (auto it = iterable.begin(); it != iterable.end(); it++) {
auto insn = it->insn;
if (insn->opcode() != OPCODE_INVOKE_DIRECT) {
continue;
}
// Check lambda-init in bytecode
if (safe_base_invoke.count(insn->get_method())) {
summary_map.emplace(insn, summary);
continue;
}
}
reaching_defs::MoveAwareFixpointIterator reaching_defs_iter(*cfg);
reaching_defs_iter.run({});
local_pointers::FixpointIterator fp_iter(*cfg);
fp_iter.run({});
auto side_effect_summary =
side_effects::SummaryBuilder(summary_map,
fp_iter,
code,
&reaching_defs_iter,
/* analyze_external_reads */ true)
.build();
return side_effect_summary.is_pure();
Step 2: Remove escaping instance
Filter out any INSTANCE that might escape and whose use we might not be able to track.
// If there is more SPUT otherthan the initial one.
if (opcode::is_an_sput(insn->opcode())) {
if (method::is_clinit(method) &&
method->get_class() == field->get_type()) {
continue;
}
// Erase if the field is written elsewhere.
concurrent_instance_map.erase(field);
continue;
}
Erase from map if this field be visited multi-times except cinit function.
Step 3: do tranformer
Remove INSTANCE from class
if (method::is_clinit(meth)) { cfg::ScopedCFG cfg(meth->get_code()); cfg::CFGMutation m(*cfg); TRACE(KOTLIN_INSTANCE, 5, "%s <clinit> before\n%s", SHOW(cls), SHOW(*cfg)); auto iterable = cfg::InstructionIterable(*cfg); for (auto insn_it = iterable.begin(); insn_it != iterable.end(); insn_it++) { auto insn = insn_it->insn; if (!opcode::is_an_sput(insn->opcode()) || insn->get_field() != field) { continue; } m.remove(insn_it); stats.kotlin_instance_fields_removed++; }
Convert INSTANCE read to new instance creation
auto move_result_it = cfg->move_result_of(insn_it); IRInstruction* new_isn = new IRInstruction(OPCODE_NEW_INSTANCE); new_isn->set_type(cls->get_type()); IRInstruction* mov_result = new IRInstruction(IOPCODE_MOVE_RESULT_PSEUDO_OBJECT); mov_result->set_dest(move_result_it->insn->dest()); IRInstruction* init_isn = new IRInstruction(OPCODE_INVOKE_DIRECT); init_isn->set_method(init)->set_srcs_size(1)->set_src( 0, move_result_it->insn->dest()); m.replace(insn_it, {new_isn, mov_result, init_isn}); m.remove(move_result_it);