Android Runtime编译优化深度解析
一、Android Runtime概述
Android Runtime(ART)是Android系统中用于执行应用程序字节码的运行环境,在Android 5.0(Lollipop)版本开始取代Dalvik成为默认运行时。ART采用AOT(Ahead-of-Time)和JIT(Just-in-Time)相结合的编译策略,相比Dalvik的JIT模式,显著提升了应用启动速度和执行效率。
从源码角度看,ART的核心代码位于Android源码的art
目录下。art/runtime
目录包含了运行时的核心实现,如对象管理、垃圾回收、线程模型等;art/compiler
目录则是编译优化的关键所在,其中定义了编译器的各个阶段和优化策略。ART的设计目标是在保证兼容性的前提下,最大限度地提升性能,同时降低内存占用和功耗。
ART的基本工作流程是:在应用安装时,通过AOT编译将字节码编译成机器码存储在设备中;在运行时,JIT编译器动态监控热点代码,对其进行即时编译优化。此外,ART还引入了Profile-guided compilation(基于配置文件的编译),通过收集应用运行时的行为数据,指导编译器进行更精准的优化。
二、ART编译架构
ART的编译架构采用模块化设计,主要分为前端(Front End)、优化阶段(Optimization Passes)和后端(Back End)三个部分。
2.1 前端
前端负责将Dalvik字节码或Java字节码解析为中间表示(Intermediate Representation,IR)。在art/compiler/optimizing/ir_builder.cc
文件中,可以看到IR构建的核心代码。例如,IrBuilder::BuildInstruction
函数负责将字节码指令转换为对应的IR节点:
cpp
void IrBuilder::BuildInstruction(const Instruction* insn) {
// 根据指令类型创建对应的IR节点
switch (insn->op_) {
case Instruction::kInvokeVirtual:
// 处理虚方法调用指令
BuildInvoke(insn, true /* is_virtual */);
break;
case Instruction::kReturn:
// 处理返回指令
BuildReturn(insn);
break;
// 其他指令类型的处理
// ...
}
}
前端还负责进行一些基础的语义检查和类型推导,确保生成的IR准确反映字节码的语义。
2.2 优化阶段
优化阶段是ART编译的核心,包含一系列优化Pass,如常量折叠、死代码消除、循环优化等。以常量折叠为例,在art/compiler/optimizing/constant_folding_pass.cc
中实现了ConstantFoldingPass
类:
cpp
class ConstantFoldingPass : public HOptimizationPass {
public:
ConstantFoldingPass(CompilerDriver* driver,
OptimizingCompilerStats* stats)
: HOptimizationPass(driver, stats, "constant-folding") {}
void Run(HGraph* graph) override {
// 遍历IR图中的所有节点
for (HInstructionIterator it(graph); !it.Done(); it.Advance()) {
HInstruction* insn = it.Current();
if (insn->IsConstantFoldable()) {
// 如果节点可进行常量折叠
HConstant* result = insn->ConstantFold();
if (result != nullptr) {
// 用折叠后的常量替换原节点
insn->ReplaceWith(result);
}
}
}
}
};
这些优化Pass会反复执行,逐步简化和优化IR,提升代码执行效率。
2.3 后端
后端负责将优化后的IR转换为目标平台的机器码。在art/compiler/backend
目录下,可以找到不同架构(如ARM、x86)的后端实现。以ARM后端为例,arm64_compiler.cc
中定义了Arm64Compiler
类,其核心函数GenerateCode
负责生成机器码:
cpp
void Arm64Compiler::GenerateCode(HGraph* graph,
CompilationUnit* unit,
Code* code) {
// 初始化代码生成器
Arm64InstructionBuilder builder(unit, code);
// 遍历IR图,生成对应的机器码指令
for (HInstructionIterator it(graph); !it.Done(); it.Advance()) {
HInstruction* insn = it.Current();
insn->GenerateCode(&builder);
}
// 完成代码生成并进行收尾工作
builder.Finish();
}
后端还会根据目标架构的特性进行针对性优化,如利用SIMD指令提升数据处理速度。
三、AOT编译原理
AOT编译是ART在应用安装阶段执行的编译过程,目的是将字节码提前编译成机器码,减少运行时的编译开销,加快应用启动速度。
3.1 AOT编译触发机制
在Android系统中,AOT编译由dex2oat
工具触发。dex2oat
工具的入口函数位于art/tools/dex2oat/dex2oat.cc
:
cpp
int main(int argc, char** argv) {
// 解析命令行参数
CommandLineFlags::SetFlagsFromCommandLine(&argc, argv, true /* remove_flags */);
// 执行AOT编译
if (!Dex2Oat().Run()) {
return 1;
}
return 0;
}
Dex2Oat::Run
函数会读取APK中的DEX文件,调用ART的编译接口进行编译。在编译过程中,会根据设备的CPU架构选择对应的后端进行机器码生成。
3.2 AOT编译流程
AOT编译的主要流程包括:
- DEX文件解析 :在
art/dex/dex_file.cc
中,DexFile::Open
函数负责打开DEX文件并解析其内容,将字节码加载到内存中。 - IR构建:与前端流程相同,将字节码转换为IR,为后续优化做准备。
- 优化与代码生成 :执行一系列优化Pass,并通过后端生成机器码。生成的机器码会存储在设备的
/data/dalvik-cache
目录下,以.odex
文件形式存在。
3.3 AOT编译的优缺点
优点:
- 加快应用启动速度,减少首次运行时的编译延迟。
- 降低运行时的CPU占用,提升整体性能。
缺点:
- 增加应用安装时间和存储空间占用。
- 由于提前编译,无法针对运行时的具体情况进行动态优化。
四、JIT编译原理
JIT编译是在应用运行时动态进行的编译过程,主要针对热点代码(频繁执行的代码段)进行即时优化,弥补AOT编译的不足。
4.1 热点代码检测
ART通过CounterTable
来实现热点代码检测。CounterTable
记录了每个方法的调用次数、循环执行次数等信息。在art/runtime/counters.cc
中,CounterTable::UpdateCounter
函数负责更新计数器:
cpp
void CounterTable::UpdateCounter(Thread* self,
const DexFile::CodeItem* code_item,
CounterId counter_id,
int32_t increment) {
// 获取对应的计数器
Counter* counter = GetCounter(self, code_item, counter_id);
// 更新计数器值
counter->Add(increment);
// 检查是否达到热点阈值
if (counter->Value() >= kHotnessThreshold) {
// 触发JIT编译
JitCompile(self, code_item);
}
}
当某个方法或代码段的计数器达到预设的热点阈值时,会触发JIT编译。
4.2 JIT编译流程
JIT编译的流程与AOT编译类似,但更注重快速响应:
- 代码片段提取:从运行时环境中提取热点代码对应的字节码。
- IR构建与优化:生成IR并执行部分关键优化Pass,如内联优化、常量传播等。
- 快速代码生成:后端生成机器码,并替换原解释执行的代码片段。
4.3 JIT编译的优势
JIT编译能够根据应用运行时的实际情况,对热点代码进行动态优化,弥补AOT编译无法适应运行时变化的缺陷。同时,JIT编译仅针对热点代码,避免了全量AOT编译带来的存储和时间开销。
五、内联优化
内联优化是ART编译优化中的重要手段,通过将被调用方法的代码直接嵌入到调用点,减少方法调用的开销。
5.1 内联决策
在art/compiler/optimizing/inline_pass.cc
中,InlinePass
类负责内联决策。ShouldInline
函数定义了内联的判断条件:
cpp
bool InlinePass::ShouldInline(HInvoke* invoke,
const DexFile::MethodId* callee_method_id) {
// 检查方法是否可内联(如非虚方法、方法体大小限制等)
if (!invoke->IsStatic() &&!invoke->IsPrivate()) {
return false;
}
if (GetMethodBytecodeSize(callee_method_id) > kMaxInlineSize) {
return false;
}
return true;
}
除了方法的可见性和大小限制外,还会考虑方法的调用频率、是否为热点方法等因素。
5.2 内联过程
当决定内联时,InlinePass::Inline
函数负责将被调用方法的代码嵌入到调用点:
cpp
void InlinePass::Inline(HInvoke* invoke,
HGraph* graph,
HBasicBlock* block) {
// 获取被调用方法的IR图
HGraph* callee_graph = GetCalleeGraph(invoke);
// 将被调用方法的代码插入到调用点
for (HInstructionIterator it(callee_graph->GetEntryBlock());!it.Done(); it.Advance()) {
HInstruction* insn = it.Current();
HInstruction* cloned_insn = insn->Clone();
block->InsertInstruction(cloned_insn);
}
// 处理参数传递和返回值
// ...
// 删除原调用指令
invoke->Remove();
}
内联优化后,减少了方法调用的栈操作和跳转开销,提升了代码执行效率。
六、循环优化
循环是程序中常见的性能瓶颈,ART针对循环进行了多种优化,如循环展开、循环不变代码外提等。
6.1 循环不变代码外提
在art/compiler/optimizing/loop_optimizations_pass.cc
中,LoopInvariantCodeMotionPass
类实现了循环不变代码外提:
cpp
class LoopInvariantCodeMotionPass : public HOptimizationPass {
public:
LoopInvariantCodeMotionPass(CompilerDriver* driver,
OptimizingCompilerStats* stats)
: HOptimizationPass(driver, stats, "loop-invariant-code-motion") {}
void Run(HGraph* graph) override {
// 遍历所有循环
for (HLoopInfo* loop_info : graph->GetLoopInfos()) {
HBasicBlock* preheader = loop_info->GetPreheader();
// 查找循环不变代码
for (HInstructionIterator it(loop_info->GetLoopHeader());!it.Done(); it.Advance()) {
HInstruction* insn = it.Current();
if (insn->IsLoopInvariant()) {
// 将不变代码移动到循环前置块
preheader->InsertInstruction(insn);
}
}
}
}
};
通过将循环不变的计算移到循环外部,减少了重复计算,提升了循环执行效率。
6.2 循环展开
循环展开通过增加每次迭代处理的数据量,减少循环控制指令的开销。在art/compiler/optimizing/loop_unrolling_pass.cc
中,LoopUnrollingPass
类实现循环展开:
cpp
class LoopUnrollingPass : public HOptimizationPass {
public:
LoopUnrollingPass(CompilerDriver* driver,
OptimizingCompilerStats* stats)
: HOptimizationPass(driver, stats, "loop-unrolling") {}
void Run(HGraph* graph) override {
// 遍历所有循环
for (HLoopInfo* loop_info : graph->GetLoopInfos()) {
// 计算展开因子
int unroll_factor = CalculateUnrollFactor(loop_info);
// 展开循环体
UnrollLoop(loop_info, unroll_factor);
}
}
};
循环展开需要在减少控制开销和增加代码体积之间进行平衡,避免过度展开导致性能下降。
七、寄存器分配
寄存器分配是将IR中的临时变量映射到目标平台寄存器的过程,合理的寄存器分配可以减少内存访问开销,提升性能。
7.1 寄存器分配算法
ART采用图着色(Graph Coloring)算法进行寄存器分配。在art/compiler/backend/register_allocator.cc
中,RegisterAllocator
类实现了寄存器分配逻辑:
cpp
class RegisterAllocator {
public:
RegisterAllocator(InstructionSelector* selector,
RegisterSet* available_registers)
: selector_(selector),
available_registers_(available_registers) {}
void AllocateRegisters() {
// 构建冲突图(Conflict Graph)
BuildConflictGraph();
// 使用图着色算法分配寄存器
ColorGraph();
// 将分配结果应用到IR
AssignRegistersToInstructions();
}
private:
// 构建冲突图
void BuildConflictGraph() {
// 遍历所有IR节点,确定变量的生命周期和冲突关系
// ...
}
// 图着色算法
void ColorGraph() {
// 尝试为每个变量分配寄存器,解决冲突
// ...
}
// 应用分配结果
void AssignRegistersToInstructions() {
// 将分配的寄存器写入IR节点
// ...
}
};
图着色算法通过为冲突图中的节点分配不同颜色(代表不同寄存器),确保在同一时间不会有两个冲突的变量使用相同的寄存器。
7.2 寄存器分配优化
为了进一步优化寄存器分配,ART还引入了寄存器溢出(Register Spilling)机制。当寄存器资源不足时,将部分变量临时存储到内存中,释放寄存器供其他变量使用。在art/compiler/backend/register_allocator.cc
中,SpillRegisters
函数负责处理寄存器溢出:
cpp
void RegisterAllocator::SpillRegisters() {
// 选择需要溢出的变量
std::vector<HValue*> variables_to_spill = SelectVariablesToSpill();
// 生成内存存储指令
for (HValue* variable : variables_to_spill) {
GenerateSpillCode(variable);
}
}
寄存器分配和溢出策略的合理设计,对代码的执行效率有着重要影响。
八、垃圾回收与编译优化
ART的垃圾回收(Garbage Collection,GC)机制与编译优化紧密结合,通过减少不必要的内存管理开销,提升整体性能。
8.1 垃圾回收算法
ART采用分代垃圾回收算法,将对象分为年轻代(Young Generation)和老年代(Old Generation),分别采用不同的回收策略。在art/runtime/gc/heap.cc
中,Heap
类实现了垃圾回收的核心逻辑:
cpp
class Heap {
public:
void CollectGarbage(GcCause gc_cause) {
// 根据垃圾回收原因选择回收策略
if (gc_cause == kGcCauseAlloc || gc_cause == kGcCauseBackground) {
// 进行年轻代回收
CollectYoungGeneration();
} else {
// 进行全量回收
CollectAllGenerations();
}
}
private:
void CollectYoungGeneration() {
// 使用复制算法(Copying Algorithm)回收年轻代
// ...
}
void CollectAllGenerations() {
// 使用标记-清除-整理(Mark-Sweep-Compact)算法回收老年代
// ...
}
};
分代回收算法能够根据对象的生命周期特性,高效地回收不再使用的对象,减少内存碎片化。
8.2 编译优化对垃圾回收的影响
编译优化可以通过减少对象创建、优化对象生命周期管理等方式,降低垃圾回收的频率和开销。例如,内联优化可以减少方法调用时临时对象的创建;循环优化可以通过减少循环体内的对象分配,降低年轻代的压力。
同时,ART的编译器会在生成代码时插入必要的垃圾回收安全点(Safe Points),确保在垃圾回收时能够正确暂停和恢复线程,保证内存状态的一致性。在art/runtime/interpreter/interpreter_common.cc
中,解释器会在合适的位置插入安全点检查:
cpp
void Interpreter::Execute() {
// 执行字节码指令
while (!IsDone()) {
Instruction* insn = FetchInstruction();
// 检查是否到达安全点
if (IsSafePoint()) {
CheckForGc();
}
ExecuteInstruction(insn);
}
}
通过编译优化与垃圾回收的协同工作,ART能够在保证内存安全的前提下,提升应用的执行效率。
九、Profile-guided compilation
Profile-guided compilation(基于配置文件的编译)是ART的一项重要优化技术,通过收集应用运行时的行为数据,指导编译器进行更精准的优化。
9.1 数据收集
ART通过Profiler
收集应用运行时的各种数据,如方法调用频率、循环执行次数、对象分配位置等。在`art
ART通过Profiler
收集应用运行时的各种数据,如方法调用频率、循环执行次数、对象分配位置等。在art/runtime/profiler/profile_saver.cc
中,ProfileSaver
类负责将收集到的数据持久化存储。例如,在记录方法调用次数时:
cpp
void ProfileSaver::RecordMethodInvocation(const DexFile::MethodId* method_id, uint32_t count) {
// 将方法ID和调用次数关联存储
method_invocation_counts_[method_id] += count;
}
这些数据会被定期写入设备存储,形成配置文件(Profile)。数据收集过程对应用性能影响较小,采用异步和轻量级的方式,避免给运行时带来过多负担 。
9.2 数据利用
在编译阶段,art/compiler/optimizing/profile_based_compilation_pass.cc
中的ProfileBasedCompilationPass
类读取配置文件数据,并将其应用到编译优化决策中。比如,根据方法调用频率进行更精准的内联决策:
cpp
bool ProfileBasedCompilationPass::ShouldInlineBasedOnProfile(HInvoke* invoke, const DexFile::MethodId* callee_method_id) {
// 从配置文件获取被调用方法的调用次数
auto it = method_invocation_counts_.find(callee_method_id);
if (it != method_invocation_counts_.end() && it->second > kHighInvocationThreshold) {
// 调用次数高时,倾向于进行内联
return true;
}
return false;
}
此外,还可以根据循环执行次数调整循环展开策略,根据对象分配热点优化内存布局等,使得编译器能针对应用实际运行特性进行优化。
9.3 动态更新与适应
随着应用运行,其行为模式可能发生变化,ART支持配置文件的动态更新。当检测到应用运行状态有显著改变(如进入不同功能模块),Profiler
会重新收集数据并更新配置文件,确保编译优化始终贴合应用当前的运行特征,实现持续的性能提升。
十、指令集优化
10.1 不同架构的指令集适配
ART针对多种硬件架构进行优化,如ARM、x86、MIPS等。在art/compiler/backend
目录下,不同架构的后端代码独立实现指令集相关优化。以ARM架构为例,在arm64_compiler.cc
中,Arm64Compiler
类会利用ARM64指令集的特性进行优化:
cpp
void Arm64Compiler::GenerateVectorInstructions(HInstruction* insn) {
// 检查是否可使用NEON指令集进行向量化操作
if (insn->IsVectorizable()) {
// 生成NEON指令,并行处理数据
GenerateNeonInstructions(insn);
}
}
对于x86架构,在x86_compiler.cc
中,X86Compiler
会利用SSE、AVX等指令集加速数据处理,通过适配不同架构的指令集,充分发挥硬件性能优势。
10.2 指令选择与调度
在代码生成阶段,art/compiler/backend/instruction_selector.cc
中的InstructionSelector
类负责从目标架构指令集中选择最合适的指令。例如,在选择算术运算指令时:
cpp
void InstructionSelector::SelectArithmeticInstruction(HInstruction* insn) {
// 根据操作数类型和目标架构特性选择指令
if (insn->GetOpCode() == HInstruction::kAdd && insn->GetInput(0)->IsInteger() && insn->GetInput(1)->IsInteger()) {
if (target_arch_ == kArm64) {
// ARM64架构下选择ADD指令
EmitArm64AddInstruction(insn);
} else if (target_arch_ == kX86) {
// x86架构下选择ADD指令
EmitX86AddInstruction(insn);
}
}
}
同时,指令调度(Instruction Scheduling)会对生成的指令进行重新排序,通过减少指令间的数据依赖和流水线停顿,进一步提升执行效率,如在arm64_instruction_scheduler.cc
中,Arm64InstructionScheduler
类实现了ARM64架构下的指令调度逻辑。
十一、内存优化相关编译策略
11.1 对象布局优化
ART通过编译优化调整对象在内存中的布局,减少内存碎片化并提升访问效率。在art/runtime/gc/heap.cc
中,对象布局的计算和调整涉及到多个因素。例如,对于类的成员变量布局,编译器会考虑数据类型和对齐要求:
cpp
size_t Heap::CalculateObjectSize(const Class* clazz) {
size_t size = kObjectHeaderSize;
// 遍历类的成员变量
for (const Field* field : clazz->GetInstanceFields()) {
size_t field_size = GetFieldSize(field);
// 计算对齐后的大小
size = Align(size, GetFieldAlignment(field));
size += field_size;
}
return size;
}
通过合理布局,减少内存空洞,使得对象占用空间更小,同时在访问成员变量时能更快速地定位。
11.2 内存访问优化
编译优化还会针对内存访问指令进行优化,减少缓存不命中(Cache Miss)。例如,在循环访问数组时,编译器会分析访问模式,尝试将连续访问的数据加载到缓存中。在art/compiler/optimizing/memory_access_optimization_pass.cc
中的MemoryAccessOptimizationPass
类会进行相关优化:
cpp
void MemoryAccessOptimizationPass::OptimizeArrayAccess(HInstruction* insn) {
if (insn->IsArrayLoad() || insn->IsArrayStore()) {
// 分析数组访问的步长和连续性
int access_stride = AnalyzeArrayAccessStride(insn);
if (access_stride == 1) {
// 连续访问时,添加预取指令
EmitPrefetchInstruction(insn);
}
}
}
预取指令提前将数据加载到缓存,减少后续访问的等待时间,提升内存访问效率。
十二、多线程相关编译优化
12.1 线程同步优化
在多线程环境下,线程同步操作(如锁操作)可能带来性能开销。ART编译器会对同步代码进行优化,在art/runtime/monitor_android.cc
中,对于synchronized
关键字对应的锁操作,编译器会尝试进行锁粗化(Lock Coarsening)和锁消除(Lock Elimination)。例如,锁粗化逻辑:
cpp
void MonitorAndroid::CoarsenLocks(Method* method) {
// 分析方法中的锁操作序列
std::vector<MonitorEnterExitPair> lock_pairs = AnalyzeLockPairs(method);
for (const auto& pair : lock_pairs) {
if (ShouldCoarsen(pair)) {
// 将相邻的锁操作合并
CombineLocks(pair);
}
}
}
通过减少锁的获取和释放次数,降低线程同步带来的开销。
12.2 并行执行优化
对于支持并行计算的代码段,ART编译器会生成支持多线程并行执行的代码。例如,在处理大规模数据计算时,编译器会将任务划分成多个子任务,利用多个线程并行处理。在art/compiler/optimizing/parallelization_pass.cc
中的ParallelizationPass
类负责相关优化:
cpp
void ParallelizationPass::ParallelizeLoop(HLoopInfo* loop_info) {
// 判断循环是否可并行化
if (IsLoopParallelizable(loop_info)) {
// 划分循环迭代任务
PartitionLoopIterations(loop_info);
// 生成多线程并行执行代码
GenerateParallelCode(loop_info);
}
}
通过并行执行,充分利用多核处理器的性能,加速任务处理。
十三、异常处理与编译优化
13.1 异常处理机制实现
ART的异常处理机制在art/runtime/exceptions.cc
中有详细实现。当异常抛出时,系统需要快速定位到异常处理代码块。编译器在生成代码时,会插入异常表(Exception Table),记录每个代码块对应的异常处理逻辑:
cpp
void Compiler::GenerateExceptionTable(Method* method) {
// 遍历方法的基本代码块
for (BasicBlock* block : method->GetBasicBlocks()) {
for (ExceptionHandler* handler : block->GetExceptionHandlers()) {
// 将异常处理信息添加到异常表
exception_table_.push_back({block, handler->GetCatchBlock(), handler->GetExceptionType()});
}
}
}
异常表使得在异常发生时,能够高效地找到对应的处理逻辑。
13.2 异常处理相关的编译优化
编译器会对异常处理代码进行优化,减少正常路径(无异常发生时的代码执行路径)的性能开销。例如,在art/compiler/optimizing/exception_handling_optimization_pass.cc
中的ExceptionHandlingOptimizationPass
类会进行以下优化:
cpp
void ExceptionHandlingOptimizationPass::OptimizeExceptionHandling(HGraph* graph) {
// 分析异常处理代码块的可达性
AnalyzeExceptionBlockReachability(graph);
// 移除不可达的异常处理代码
RemoveUnreachableExceptionHandlers(graph);
// 合并重复的异常处理逻辑
MergeDuplicateExceptionHandlers(graph);
}
通过这些优化,在保证异常处理正确性的同时,提升应用正常运行时的性能表现。