Android Runtime内联优化策略源码级解析(91)

91 阅读6分钟

Android Runtime内联优化策略源码级解析

一、Android Runtime基础

Android Runtime(ART)自Android 5.0起成为应用运行核心,采用AOT与JIT结合的编译方式。ART架构涵盖类加载器、解释器、JIT编译器、垃圾回收器等模块。类加载器负责加载字节码文件并初始化类;解释器在程序启动初期逐行解释执行字节码;JIT编译器监控热点代码,将其编译为机器码。内联优化在JIT编译过程中发挥作用,位于art/runtime/compiler目录下的相关源码,是实现内联优化的关键。

二、内联优化原理

内联优化是将被调用方法的代码直接插入调用点,消除方法调用开销。常规方法调用需保存上下文、传递参数、跳转执行、恢复上下文等操作,而内联后程序可直接执行方法体代码。例如:

class InlineDemo {
    int add(int a, int b) {
        return a + b;
    }
    void test() {
        int result = add(2, 3);
    }
}

内联后test方法变为int result = 2 + 3;,减少了方法调用步骤。同时,内联还能为常量折叠、死代码消除等全局优化创造条件,提高指令缓存命中率 。

三、内联触发条件

3.1 方法调用频率

ART的JIT编译器在runtime/jit/compiler_driver.cc中监控方法调用次数。在CompilerDriver::ProcessCompilationUnit函数中,当方法调用次数达到kInlineInvocationThreshold阈值时,会将方法标记为可能内联:

if (method->GetInvocationCount() >= kInlineInvocationThreshold) {
    method->SetIsCandidateForInlining(true);
}

阈值设置需平衡程序特性与设备性能,过低易导致代码膨胀,过高则会错过优化机会。

3.2 方法大小限制

runtime/compiler/inline_state.ccInlineState::ShouldInline函数会检查方法大小:

size_t method_size = method->GetBytecodeSize();
if (method_size > kMaxInlineSize) {
    return false;
}

kMaxInlineSize为最大内联方法大小阈值,避免因方法过大导致代码膨胀,影响内存和指令缓存效率。

3.3 方法类型与可见性

同样在InlineState::ShouldInline函数中,私有方法和静态方法更易内联,因为调用范围固定,便于分析。对于虚方法,需判断调用点是否有足够信息确定具体实现:

if (method->IsPrivate()) {
    return true;
}
if (method->IsStatic()) {
    return true;
}
if (method->IsVirtual()) {
    if (!HasEnoughInfoForVirtualInline(method, call_site)) {
        return false;
    }
}

四、内联决策过程

4.1 构建调用图

runtime/compiler/graph_builder.ccGraphBuilder::BuildGraph函数遍历字节码指令,识别方法调用,构建调用图:

for (InstructionIterator it(method, dex_file, code_item);!it.Done(); it.Advance()) {
    const Instruction* inst = it.CurrentInstruction();
    if (inst->IsInvoke()) {
        const DexFile::MethodId* method_id = inst->GetMethodIndex();
        const DexFile::Method* method = dex_file->LookupMethod(method_id);
        call_graph.AddEdge(current_method, method);
    }
}

通过调用图,编译器明确方法调用关系,为内联决策提供依据。

4.2 分析调用点上下文

runtime/compiler/inline_state.ccInlineState::AnalyzeCallSite函数分析调用点参数类型、返回值类型、所在方法执行状态(如是否在循环、异常处理块中):

const std::vector<mirror::Class*>& argument_types = call_site->GetArgumentTypes();
for (size_t i = 0; i < argument_types.size(); ++i) {
    if (!IsTypeCompatible(argument_types[i], method->GetParameterTypes()[i])) {
        has_type_mismatch = true;
    }
}
if (call_site->IsInLoop()) {
    loop_nesting_depth = call_site->GetLoopNestingDepth();
}
if (call_site->IsInExceptionHandler()) {
    in_exception_handler = true;
}

这些信息用于评估内联可行性与潜在影响。

4.3 计算收益与成本

runtime/compiler/inline_heuristics.ccInlineHeuristics::ComputeInlineBenefit函数计算内联收益,考虑调用次数、方法大小、额外优化机会等因素:

double base_benefit = method->GetInvocationCount() * kInvocationBenefitFactor;
double size_cost_factor = static_cast<double>(method->GetBytecodeSize()) / kMaxInlineSize;
if (CanPerformAdditionalOptimizationsAfterInlining(method)) {
    base_benefit *= kAdditionalOptimizationBenefitFactor;
}
double inline_benefit = base_benefit * (1 - size_cost_factor);

InlineHeuristics::ComputeInlineCost函数计算成本,包括代码膨胀和指令缓存影响:

double code_size_cost = static_cast<double>(method->GetBytecodeSize()) * kCodeSizeCostFactor;
if (WillInlineAffectInstructionCache(method, call_site)) {
    code_size_cost *= kInstructionCacheCostFactor;
}
double inline_cost = code_size_cost;

当收益大于成本时,在runtime/compiler/inline_state.ccInlineState::ShouldInline函数中决定内联。

五、内联代码生成

5.1 提取被调用方法代码

runtime/compiler/inline_code_generator.ccInlineCodeGenerator::ExtractMethodCode函数提取字节码指令:

const uint8_t* bytecode = method->GetCode();
size_t bytecode_size = method->GetBytecodeSize();
std::vector<uint8_t> extracted_code(bytecode_size);
memcpy(extracted_code.data(), bytecode, bytecode_size);

若方法已JIT编译,则从代码缓存获取机器码。

5.2 调整代码

调整变量引用和跳转指令。InlineCodeGenerator::AdjustVariableReferences函数重新映射变量索引:

for (size_t i = 0; i < extracted_code.size(); ++i) {
    const Instruction* inst = reinterpret_cast<const Instruction*>(&extracted_code[i]);
    if (inst->IsVariableReference()) {
        uint32_t variable_index = inst->GetVariableIndex();
        uint32_t new_variable_index = RemapVariableIndex(variable_index, call_site->GetMethod(), method);
        inst->SetVariableIndex(new_variable_index);
    }
}

InlineCodeGenerator::AdjustJumpInstructions函数调整跳转目标地址:

for (size_t i = 0; i < extracted_code.size(); ++i) {
    const Instruction* inst = reinterpret_cast<const Instruction*>(&extracted_code[i]);
    if (inst->IsJumpInstruction()) {
        int32_t jump_offset = inst->GetJumpOffset();
        int32_t new_jump_address = CalculateNewJumpAddress(jump_offset, call_site->GetCodeOffset(), i);
        inst->SetJumpOffset(new_jump_address - call_site->GetCodeOffset());
    }
}

5.3 代码融合

InlineCodeGenerator::InsertInlineCode函数将调整后的代码插入调用点:

size_t call_site_offset = call_site->GetCodeOffset();
std::vector<uint8_t>& target_code = call_site->GetMethod()->GetCodeBuffer();
target_code.insert(target_code.begin() + call_site_offset, extracted_code.begin(), extracted_code.end());

六、内联与其他优化协同

内联优化与常量折叠、死代码消除等紧密配合。内联后,runtime/compiler/optimizing_compiler.cc中常量折叠优化可识别更多常量表达式。如内联后return x + 5x为编译期常量)可优化为具体数值。死代码消除也因内联扩大代码分析范围,更易识别无用代码并删除 。

七、内联对性能与内存的影响

内联减少方法调用开销,提升执行速度,但可能导致代码膨胀,占用更多内存。通过合理设置内联阈值和限制方法大小,可在性能提升与内存占用间取得平衡。在循环等高频调用场景,内联能显著提升效率;而对大方法过度内联,则可能因代码膨胀降低指令缓存命中率,反而影响性能。

八、虚方法内联特殊处理

虚方法因运行时可能被重写,内联难度大。ART通过分析调用点上下文,判断是否能确定具体实现方法。若能确定,如在类的内部调用自身未被重写的虚方法,可进行内联;否则需谨慎处理,避免错误内联导致运行时异常。

九、递归方法内联策略

对于递归方法,直接内联可能导致代码无限膨胀。ART会设定递归深度限制,当递归深度超过一定阈值时,不进行内联。同时,通过分析递归方法的特性,如尾递归,在满足条件时将其转换为迭代形式后再考虑内联,以实现优化同时避免代码膨胀问题。

十、不同设备架构下的内联优化差异

不同设备架构(如ARM、x86)的指令集和内存特性不同,ART针对这些差异调整内联优化策略。在ARM架构上,考虑其寄存器数量和指令特点,优化内联过程中的寄存器分配和代码布局;在x86架构上,则根据其指令执行效率和内存寻址方式,调整内联代码生成和优化方式,以充分发挥各架构性能优势。