美文网首页
ART中的IR优化

ART中的IR优化

作者: HAPPYers | 来源:发表于2020-03-23 16:03 被阅读0次

以Android7.0 源码树为例,边翻源码边看ART在把dex编译为机器码的时候,做了哪些IR优化。

除去对特定CPU架构的优化,ART为SSA形式化后的CFG设计了13种不同的优化方法。(有的优化方法被执行了不止一次)

IR优化的入口函数
art\compiler\optimizing\optimizing_compiler.cc -> RunOptimizations

static void RunOptimizations(HGraph* graph,
                             CodeGenerator* codegen,
                             CompilerDriver* driver,
                             OptimizingCompilerStats* stats,
                             const DexCompilationUnit& dex_compilation_unit,
                             PassObserver* pass_observer,
                             StackHandleScopeCollection* handles) {
  ArenaAllocator* arena = graph->GetArena();
  HDeadCodeElimination* dce1 = new (arena) HDeadCodeElimination(
      graph, stats, HDeadCodeElimination::kInitialDeadCodeEliminationPassName);
  HDeadCodeElimination* dce2 = new (arena) HDeadCodeElimination(
      graph, stats, HDeadCodeElimination::kFinalDeadCodeEliminationPassName);
  HConstantFolding* fold1 = new (arena) HConstantFolding(graph);
  InstructionSimplifier* simplify1 = new (arena) InstructionSimplifier(graph, stats);
  HSelectGenerator* select_generator = new (arena) HSelectGenerator(graph, stats);
  HConstantFolding* fold2 = new (arena) HConstantFolding(graph, "constant_folding_after_inlining");
  HConstantFolding* fold3 = new (arena) HConstantFolding(graph, "constant_folding_after_bce");
  SideEffectsAnalysis* side_effects = new (arena) SideEffectsAnalysis(graph);
  GVNOptimization* gvn = new (arena) GVNOptimization(graph, *side_effects);
  LICM* licm = new (arena) LICM(graph, *side_effects, stats);
  LoadStoreElimination* lse = new (arena) LoadStoreElimination(graph, *side_effects);
  HInductionVarAnalysis* induction = new (arena) HInductionVarAnalysis(graph);
  BoundsCheckElimination* bce = new (arena) BoundsCheckElimination(graph, *side_effects, induction);
  HSharpening* sharpening = new (arena) HSharpening(graph, codegen, dex_compilation_unit, driver);
  InstructionSimplifier* simplify2 = new (arena) InstructionSimplifier(
      graph, stats, "instruction_simplifier_after_bce");
  InstructionSimplifier* simplify3 = new (arena) InstructionSimplifier(
      graph, stats, "instruction_simplifier_before_codegen");
  IntrinsicsRecognizer* intrinsics = new (arena) IntrinsicsRecognizer(graph, driver, stats);

  HOptimization* optimizations1[] = {//第一轮优化,总共5遍
    intrinsics,
    sharpening,
    fold1,
    simplify1,
    dce1,
  };
  RunOptimizations(optimizations1, arraysize(optimizations1), pass_observer);

  MaybeRunInliner(graph, codegen, driver, stats, dex_compilation_unit, pass_observer, handles);

  HOptimization* optimizations2[] = {
//第二轮优化,共12遍
    // SelectGenerator depends on the InstructionSimplifier removing
    // redundant suspend checks to recognize empty blocks.
    select_generator,
    fold2,  // TODO: if we don't inline we can also skip fold2.
    side_effects,
    gvn,
    licm,
    induction,
    bce,
    fold3,  // evaluates code generated by dynamic bce
    simplify2,
    lse,
    dce2,
    // The codegen has a few assumptions that only the instruction simplifier
    // can satisfy. For example, the code generator does not expect to see a
    // HTypeConversion from a type to the same type.
    simplify3,
  };
  RunOptimizations(optimizations2, arraysize(optimizations2), pass_observer);

  RunArchOptimizations(driver->GetInstructionSet(), graph, codegen, stats, pass_observer);
  AllocateRegisters(graph, codegen, pass_observer);
}

下面举几个优化的例子

IntrinsicsRecognizer

art\compiler\optimizing\intrinsics.cc => IntrinsicsRecognizer::Run()

IntrinsicsRecognizer识别基本块中可以被intrinsic化的函数调用指令,然后把intrinsic信息设置到该指令中。在后续翻译为机器指令的时候可以实现intrinsic优化。

DexFileMethodInliner::kIntrinsicMethods数组中定义了ART中支持的Intrinsics方法和Inline方法。
art\compiler\dex\quick\dex_file_method_inliner.cc

const DexFileMethodInliner::IntrinsicDef DexFileMethodInliner::kIntrinsicMethods[] = {
#define INTRINSIC(c, n, p, o, d) \
    { { kClassCache ## c, kNameCache ## n, kProtoCache ## p }, { o, kInlineIntrinsic, { d } } }

    INTRINSIC(JavaLangDouble, DoubleToRawLongBits, D_J, kIntrinsicDoubleCvt, 0),
    INTRINSIC(JavaLangDouble, LongBitsToDouble, J_D, kIntrinsicDoubleCvt, kIntrinsicFlagToFloatingPoint),
 ...
    INTRINSIC(JavaLangInteger, ReverseBytes, I_I, kIntrinsicReverseBytes, k32),
    INTRINSIC(JavaLangLong, ReverseBytes, J_J, kIntrinsicReverseBytes, k64),
    INTRINSIC(JavaLangShort, ReverseBytes, S_S, kIntrinsicReverseBytes, kSignedHalf),
    INTRINSIC(JavaLangInteger, Reverse, I_I, kIntrinsicReverseBits, k32),
    INTRINSIC(JavaLangLong, Reverse, J_J, kIntrinsicReverseBits, k64),

    INTRINSIC(JavaLangInteger, BitCount, I_I, kIntrinsicBitCount, k32),
    INTRINSIC(JavaLangLong, BitCount, J_I, kIntrinsicBitCount, k64),
    INTRINSIC(JavaLangInteger, Compare, II_I, kIntrinsicCompare, k32),
    INTRINSIC(JavaLangLong, Compare, JJ_I, kIntrinsicCompare, k64),
    INTRINSIC(JavaLangInteger, HighestOneBit, I_I, kIntrinsicHighestOneBit, k32),
    INTRINSIC(JavaLangLong, HighestOneBit, J_J, kIntrinsicHighestOneBit, k64),
    INTRINSIC(JavaLangInteger, LowestOneBit, I_I, kIntrinsicLowestOneBit, k32),
    INTRINSIC(JavaLangLong, LowestOneBit, J_J, kIntrinsicLowestOneBit, k64),
    INTRINSIC(JavaLangInteger, NumberOfLeadingZeros, I_I, kIntrinsicNumberOfLeadingZeros, k32),
    INTRINSIC(JavaLangLong, NumberOfLeadingZeros, J_I, kIntrinsicNumberOfLeadingZeros, k64),
 ...

#define UNSAFE_GET_PUT(type, code, type_flags) \
    INTRINSIC(SunMiscUnsafe, Get ## type, ObjectJ_ ## code, kIntrinsicUnsafeGet, \
              type_flags), \
    INTRINSIC(SunMiscUnsafe, Get ## type ## Volatile, ObjectJ_ ## code, kIntrinsicUnsafeGet, \
              type_flags | kIntrinsicFlagIsVolatile), \
    INTRINSIC(SunMiscUnsafe, Put ## type, ObjectJ ## code ## _V, kIntrinsicUnsafePut, \
              type_flags), \
    INTRINSIC(SunMiscUnsafe, Put ## type ## Volatile, ObjectJ ## code ## _V, kIntrinsicUnsafePut, \
              type_flags | kIntrinsicFlagIsVolatile), \
    INTRINSIC(SunMiscUnsafe, PutOrdered ## type, ObjectJ ## code ## _V, kIntrinsicUnsafePut, \
              type_flags | kIntrinsicFlagIsOrdered)

    UNSAFE_GET_PUT(Int, I, kIntrinsicFlagNone),
    UNSAFE_GET_PUT(Long, J, kIntrinsicFlagIsLong),
    UNSAFE_GET_PUT(Object, Object, kIntrinsicFlagIsObject),
#undef UNSAFE_GET_PUT

    // 1.8
    INTRINSIC(SunMiscUnsafe, GetAndAddInt, ObjectJI_I, kIntrinsicUnsafeGetAndAddInt, 0),
    INTRINSIC(SunMiscUnsafe, GetAndAddLong, ObjectJJ_J, kIntrinsicUnsafeGetAndAddLong, 0),
    INTRINSIC(SunMiscUnsafe, GetAndSetInt, ObjectJI_I, kIntrinsicUnsafeGetAndSetInt, 0),
    INTRINSIC(SunMiscUnsafe, GetAndSetLong, ObjectJJ_J, kIntrinsicUnsafeGetAndSetLong, 0),
    INTRINSIC(SunMiscUnsafe, GetAndSetObject, ObjectJObject_Object, kIntrinsicUnsafeGetAndSetObject, 0),
    INTRINSIC(SunMiscUnsafe, LoadFence, _V, kIntrinsicUnsafeLoadFence, 0),
    INTRINSIC(SunMiscUnsafe, StoreFence, _V, kIntrinsicUnsafeStoreFence, 0),
    INTRINSIC(SunMiscUnsafe, FullFence, _V, kIntrinsicUnsafeFullFence, 0),

    INTRINSIC(JavaLangSystem, ArrayCopy, CharArrayICharArrayII_V , kIntrinsicSystemArrayCopyCharArray,
              0),
    INTRINSIC(JavaLangSystem, ArrayCopy, ObjectIObjectII_V , kIntrinsicSystemArrayCopy,
              0),

    INTRINSIC(JavaLangInteger, RotateRight, II_I, kIntrinsicRotateRight, k32),
    INTRINSIC(JavaLangLong, RotateRight, JI_J, kIntrinsicRotateRight, k64),
    INTRINSIC(JavaLangInteger, RotateLeft, II_I, kIntrinsicRotateLeft, k32),
    INTRINSIC(JavaLangLong, RotateLeft, JI_J, kIntrinsicRotateLeft, k64),

#undef INTRINSIC

//SPECIAL宏用于定义Inline函数
#define SPECIAL(c, n, p, o, d) \
    { { kClassCache ## c, kNameCache ## n, kProtoCache ## p }, { o, kInlineSpecial, { d } } }

    SPECIAL(JavaLangString, Init, _V, kInlineStringInit, 0),
    SPECIAL(JavaLangString, Init, ByteArray_V, kInlineStringInit, 1),
    SPECIAL(JavaLangString, Init, ByteArrayI_V, kInlineStringInit, 2),
    SPECIAL(JavaLangString, Init, ByteArrayII_V, kInlineStringInit, 3),
    SPECIAL(JavaLangString, Init, ByteArrayIII_V, kInlineStringInit, 4),
    SPECIAL(JavaLangString, Init, ByteArrayIIString_V, kInlineStringInit, 5),
...
#undef SPECIAL
};

这其中,
Intrinsics方法支持了

  • java.lang包中的Float,Double,Integer,Math等类的很多函数
  • sun.misc.unSafe类中的一些函数

Inline函数支持了

  • java.lang.String的构造函数(被标记为kInlineSpecial)
  • 部分用户定义的方法

HConstantFolding & InstrutionSimplifier

HConstantFolding 包括常量折叠和常量传播的优化。
InstrutionSimplifier暂略

HDeadCodeElimination

HDeadCodeElimination用于消除CFG中无效的基本块和无用的HInstruction
art\compiler\optimizing\dead_code_elimination.cc

HSelectGenerator

select有些类似c语言的?:三目运算符。
例如下面的程序

public class OatSelectTest{
    int x=0, y=0, z=0, w=0;
    public void test() {
        int ret = 666;
        if( x<0 ){
            ret=-ret;
        }else{
            ret+=123;
        }
        x=ret;
    }
}

经过select优化后会变成

public class OatSelectTest{
    int x=0, y=0, z=0, w=0;
    public void test() {
        int ret = 666;
        ret1 = -666;
        ret2 = 789;
        x=select(ret1,ret2,x<0?);
    }
}

而在机器指令生成层面,以x86为例,可以省略一次jmp指令跳转,且可以借助x86的cmov(conditional move)指令来简化。

SideEffectsAnalysis

SideEffectsAnalysis其实不是一种优化,它只是收集合并CFG中各个基本块包含指令的SideEffects信息。SideEffects用于描述ART IR指令的副作用(SideEffects)。例如 HNewInstance(new一个对象)就有Trigger GC的副作用,即HNewInstance指令的执行可能会出发垃圾回收。

这也和后面所说的(见总结),ART编译完本地机器码后,程序的运行仍然不能脱离具体虚拟机的实现。
就好比此处的SideEffects,当然也有很多必要的特殊指令在ART编译dex为机器码的时候会添加上,使得得到的机器码在运行的过程中其实离不开虚拟机的管控和支持。
例如

  • Java虚拟机的垃圾回收器做对象标记前,往往会设置一个标志,表示自己要做对象标记了。
  • 其他线程运行的时候要经常对这个标志进行检查,发现这个标志有效时,这些线程就需要等待,让垃圾回收器能安全地做对象标记。否则,GC一边做对象标记,线程又同时创建对象,修改引用,这会导致对象标记不准确,影响垃圾回收。
    这些检查标记地添加时编译器主动完成的。它会在两个地方添加标记检查指令,一个是在Entry基本块中,另一个是在Loop header基本块里。
    art\compiler\optimizing\instruction_builder.cc => HInstructionBuilder::Build
bool HInstructionBuilder::Build() {
  locals_for_.resize(graph_->GetBlocks().size(),
                     ArenaVector<HInstruction*>(arena_->Adapter(kArenaAllocGraphBuilder)));
...
  for (HReversePostOrderIterator block_it(*graph_); !block_it.Done(); block_it.Advance()) {
    current_block_ = block_it.Current();
    uint32_t block_dex_pc = current_block_->GetDexPc();

    InitializeBlockLocals();

    if (current_block_->IsEntryBlock()) {
      InitializeParameters();
      AppendInstruction(new (arena_) HSuspendCheck(0u));
      AppendInstruction(new (arena_) HGoto(0u));
      continue;
    } else if (current_block_->IsExitBlock()) {
...
    } else if (current_block_->IsLoopHeader()) {
      HSuspendCheck* suspend_check = new (arena_) HSuspendCheck(current_block_->GetDexPc());
...
    }
...
}

这其中就设置了HSuspendCheckHGoto标志。
SuspendCheck而言,每次循环都会执行SuspendCheck检查。因为如果一个线程在循环里执行时间过长,会导致对象标记不能开展,进而影响GC。

GVNOptimization

Global Value Numbering(全局值编号)优化。
值编号有全局和局部之分。

  • 局部(Local) VN是针对单个基本块内的值编号。
  • 全局(Global) VN可以跨基本块,但要求CFG先SSA形式化。在ART中,因为CFG已经SSA了,此处使用GVN

值编号的意思就是给表达式Ex设置一个编号,如果代码中有其他表达式Ey能计算出和Ex相同值的话,那么Ey的值可以用Ex替代。
VN的效果简而言之就是消除/替换冗余代码
例如

w = 3; // 这是一个表达式,由于值编号系统还没有这个表达式,所以这个表达式会被添加到值编号系统中。
x =  3; // 这也是一个表达式,但计算出来的值在值编号系统中存在,所以可以改写为 y= w

y = x + 4 // 这个表达式可以替换x为w, 得到 y = w + 4这样一个新表达式,并添加到值编号系统
z = w + 4 //这个表达式和上个(y=w+4)值一样,所以z=y

上述代码经过值编号之后, x=3, z=w+4可以被其他代码替换,这两个表达式可以消除。
实现值编号的时候,有两个问题

  • 如何给表达式编号? ART中,用ART IR(HInstruction对象)的hash code作为该IR的编号
  • 如何判断两个表达式等价? ART中,通过HInstructionEquals函数来判断。其实现包括对Constant的比较,对数据类型的比较,对各输入参数的个数与值的比较。
bool HInstruction::Equals(HInstruction* other) const {
//比较指令类型
  if (!InstructionTypeEquals(other)) return false;
  DCHECK_EQ(GetKind(), other->GetKind());
// 如果有值,则比较
  if (!InstructionDataEquals(other)) return false;
// 指令执行结果的数据类型的比较
  if (GetType() != other->GetType()) return false;
// 输入参数个数要一样
  if (InputCount() != other->InputCount()) return false;
// 比较各个输入参数
  for (size_t i = 0, e = InputCount(); i < e; ++i) {
    if (InputAt(i) != other->InputAt(i)) return false;
  }
  DCHECK_EQ(ComputeHashCode(), other->ComputeHashCode());
  return true;
}

GVN的具体实现在art\compiler\optimizing\gvn.cc

void GlobalValueNumberer::Run() {
  DCHECK(side_effects_.HasRun());
  sets_[graph_->GetEntryBlock()->GetBlockId()] = new (allocator_) ValueSet(allocator_);

  // Use the reverse post order to ensure the non back-edge predecessors of a block are
  // visited before the block itself.
  for (HReversePostOrderIterator it(*graph_); !it.Done(); it.Advance()) {
    VisitBasicBlock(it.Current());
  }
}

VisitBasicBlock函数进行关键的PRO遍历CFG,并进行值系统中的判断,消除操作。
我们可以在Lookup函数中看出之前的hash比较操作.

  // If in the set, returns an equivalent instruction to the given instruction.
  // Returns null otherwise.
  HInstruction* Lookup(HInstruction* instruction) const {
    size_t hash_code = HashCode(instruction);
    size_t index = BucketIndex(hash_code);

// buckets_[index] 链表保存了所有相同hash_code的HInstruction
    for (Node* node = buckets_[index]; node != nullptr; node = node->GetNext()) {
      if (node->GetHashCode() == hash_code) {
        HInstruction* existing = node->GetInstruction();
        if (existing->Equals(instruction)) {
          return existing;
        }
      }
    }
    return nullptr;
  }

LICM

Loop Invariant Code Motion.
循环不变量外提 。顾名思义。

HInductionAnalysis

Induction Variable Analysis 归纳变量分析。
归纳变量可分为基本归纳变量和派生归纳变量。
通过一个例子来了解它们

    i = 0
L1: if i>n goto L2
    j = i + 4
    k = j + a
    i = i+ 1
    goto L1
L2: ......

上述代码有i,j,k三个变量。

  • i变量取值满足i=i+c这样的公式。其中c是循环不变量/表达式 (c的值不受循环影响)。此处c为.这样的变量称为基本归纳变量。基本归纳变量可以用一个三元组i=<i,1,0>表达,即i=i*1+0
  • 围绕基本归纳变量可以得到和它相关的派生变量。例如这里的j满足j=a*i+b(a,b也是循环不变量)公式,k也是如此。此处,j直接依赖于i,k通过j间接依赖i,所以j,k都是由i派生的派生归纳变量。派生归纳变量可以用三元组x=<i,a,b>表达,即x=a*i+b

识别归纳变量后,存在多种情况优化我们的代码

  • 强度削减优化。例如用加法替代减法。
  • 强度削减优化后,可以消除一些循环中的归纳变量。

例如对下面代码进行归纳变量分析优化

while (i<10){
    j=i*9+3;
    a[j]=a[j]+5;
    i+=3;
}

强度削减后(将乘法变为加法)

s=9*i+3;
while (i<10){
    j=s;
    a[j]=a[j]+5;
    i+=3;
    s+=27;
}

而后消除冗余的归纳变量

s=9*i+3;
while (s<93/*i<10*/){
    //j=s;
    a[s]=a[s]+5;
    //i+=3;
    s+=27;
}

其他优化方法

HBoundsCheckElimination

ART在翻译aget,aput(数组圆度读/取指令)为IR的HArrayGet或者HArraySet的时候,同时还会生成一条HBoundsCheck IR,用于检查是否索引越界。在一个循环中,这个检查开销很大。HBoundsCheckElimination可以消除这种不必要的边界检查。例如循环索引最大值本身不超过数组元素个数的时候,那么就能去掉HBoundsCheck.

LoadStoreElimination

例如某个地方已经获取了一个类成员的变量值,后续再通过HInstanceFieldGet IR获取同一个成员变量的时候就可以消除这些重复指令了。

总结

  • ART IR中的编号是以偶数倍递增的,这是为了方便在IR之间插入一些指令(检查或者标志),而后这些插入指令的编号就是奇数了。
  • 虽然ART把dex字节码编译为了本地机器码,但是其运行仍然不像其他例如c++编译为字节码直接在系统或者运行时库中运行。ART编译完本地机器码后,程序的运行仍然不能脱离具体虚拟机的实现。因为有一些检查标志需要虚拟机进行interpreter,且需要虚拟机的管控。(前文SideEffectsAnalysis的分析中有介绍)

相关文章

  • ART中的IR优化

    以Android7.0 源码树为例,边翻源码边看ART在把dex编译为机器码的时候,做了哪些IR优化。 除去对特定...

  • Android Q新特性(二)-ART虚拟机优化

    android Q 对ART的优化 简单说两大优化,提升性能 JIT优化Android Q 大幅改进了 ART 运...

  • 数字IC设计后端实现前期预防IR Drop的方法汇总

    谈到数字IC后端实现中IR drop,大家都知道,它是指电压降。IR Drop的分析包含静态IR Drop和动态I...

  • 零散知识

    android 8 art优化 https://source.android.google.cn/devices/...

  • 代码混淆

    LLVM编译过程: 预处理,词法分析,token,语法分析,AST,代码生成,LLVM IR,优化,生成,汇编代码...

  • llvm cookbook 1.7 优化ir

    本文介绍如何使用opt工具优化llvm ir。 使用之前编写的代码 multiply.c 执行命令 生成 mult...

  • IR知识总结

    什么是IR? IR分为SSA(static single assignment 静态单赋值) IR和 正常IR经过...

  • docker容器commit之后变得非常大的原因及解决办法

    优化 docker 容器大小: https://blog.csdn.net/weixin_43220532/art...

  • ART虚拟机学习

    重点请参考Android运行时ART简要介绍和学习计划 包含类加载和方法的执行 一、ART对APK的优化 APK里...

  • IR drop ANT EM

    IR压降(IR-Drop) IR压降是指出现在集成电路中电源和地网络上电压下降或升高的一种现象。随着半导体工艺的演...

网友评论

      本文标题:ART中的IR优化

      本文链接:https://www.haomeiwen.com/subject/phoiyhtx.html