• C2 EA


    https://www.usenix.org/legacy/events/vee05/full_papers/p111-kotzmann.pdf

    https://dl.acm.org/doi/10.1145/320385.320386

    论文的简版,主要是重要的摘要,和一些个人的观点,酌情采纳⚠️。

    连接图符号定义

    首先给一些连接图里面的符号的定义,以及简单的连接图长啥样。
    image

    • a=new T1,就表示为a--PointsTo-->T1

    • b=a表示为b-->DeferredTo-->b

    • a.f=q 可以表示为PointsTo(a)--FieldTo-->f,f--DeferredTo-->q

    • a=b.f可以表示为PointsTo(b)--FieldTo-->f,a--DeferredTo-->f

    连接图

    接下来正式描述用来建模程序的连接图CG

    源码:
    image
    image
    CG:
    image
    论文主要围绕这幅图展开。

    1. 方法入口的CG

    假设形参是fi,实参是ai,方法入口就像是有fi = ai这样的代码。因为Java是传值所以fi可以视作方法局部变量。我们为ai创建一个phantom node(见图B),从fi到ai用defered边连接。然后初始化ai和fi逃逸状态,即:EscapeState[fi]=NoEscape ,EscapeState[ai]=ArgEscape

    2. 方法结束的CG

    执行可达性分析。从GlobalEscape和ArgEscape状态的节点出发,可达的节点构成NonLocalGraph,可以逃逸。从NoEscape出发,可达的节点构成LocalGraph,不可逃逸。其中最初标记为GlobalEscape的节点有static fields和Runnable对象,最初被标记为ArgEscape的节点有哪些表示实参的phantom nodes,比如图B的a1和a2。

    3. 调用前的CG

    用ai作为实参,ai表示形参。假如有个调用u1.foo(u2,u3,...un),我们把它写成a1=u1;a2=u2;...;foo(a1,a2,...)。换句话说,创建了一堆ai的phantom node,它们和实参以DeferredTo连接。咋一看可能没必要,实际上这是后面更新caller cg必备的。

    4. 调用后的CG

    此时我们需要根据callee分析的结果来更新caller的cg,包括节点的更新和边的更新。更新节点算法如下。
    image
    caller的ai^对应callee的ai,我们从这个出发,寻找callee节点对应的caller节点,如果找不到就创建一个NoEscape的caller节点和它的field节点,然后把这些field节点再update一次。

    例子参见图F,我们从a1和a1出发,将它们作为最初的“field”节点。然后将a1指向的S4映射到a1指向的S0.接下面有个环会导致将S1映射为S4。同时更新S1和S4的next节点(field节点),互相指向。

    EA具体实现

    C2的ea概念上遵从论文,高度一致,但是实现上和论文还是有很大区别的。它属于flow-insensitive的分析,代码主要位于ConnectionGraph::compute_escape。可以分为五个部分:

    1. 创建CG的点和边
    2. 完成CG构造
    3. 调整节点的标量替换状态
    4. 基于EA分析的结果优化IR
    5. 精确化内存节点的类型范围

    最核心的过程是前两步,后面三步是利用前两步建模的CG来做优化。说白了,就是用CG建模Ideal graph,让每个IR中重要的node(EA关心的那些操作,比如局部变量创建)都能在CG中有对应的PointsTo。

    1. 创建CG的点和边

    bool ConnectionGraph::compute_escape() {
      ....
      
      // 1. 产生CG,该图由PointsTo节点组成
      ideal_nodes.map(C->live_nodes(), NULL);  // preallocate space
     // =====从Root节点出发,遍历整个IR
      if (C->root() != NULL) {
        ideal_nodes.push(C->root());
      }
      ptnodes_worklist.append(phantom_obj);
      java_objects_worklist.append(phantom_obj);
      for( uint next = 0; next < ideal_nodes.size(); ++next ) {
        Node* n = ideal_nodes.at(next);
        // =====每次拿到IR中的节点n,就创建n对应的PointsToNode,然后把它加到CG。
        // =====CG的所有节点都是PointsToNode(及其子类)
        add_node_to_connection_graph(n, &delayed_worklist);
        ...
      // =====补充之前不完整的CG
      while(delayed_worklist.size() > 0) {
        Node* n = delayed_worklist.pop();
        add_final_edges(n);
      }
      
      ...
    }
    

    c2从Root节点出发遍历整个IR图,遇到每个节点都先调用add_node_to_connection_graph初步构造出CG的“草图”。为什么说是草图呢?因为add_node_to_connection_graph这里创建的PointsToNode可能是不完整的,比如对于CMove节点,它只会创建CMove节点本身对应的PointsToNode,但是CMove输入节点到CMove的边不会创建,因为它的输入节点对应的PointsToNode可能还没有构建,需要放到delayed_worklist。

    在add_final_edges的时候,会从delayed_workerlist里面拿节点,这时候CMove和CMove输入对应的PointsToNode都创建完毕了,所以这个时候可以创建这些PointsToNode的边,即CG中的边,这一步完成之后“草图”就变成了可用的图了。

    除了CMove之外,很多节点都需要add_node_to_connection_graph和add_final_edges两步处理。这里讨论一下最重要的Call节点。在add_node_to_connection_graph第一次处理Call节点的时候,它检查:

    1. Call是否返回新创建的未逃逸的对象,如果是,则创建一个表示Java对象的PointsNode(即JavaObjectNode),然后将它标记为NoEscape
    2. Call是否返回参数,如果是,则将Call节点映射为一个PointsNode,然后标记为ArgEscape
    3. 否则,将Call节点映射为一个phantom_obj(即未知对象)

    换句话说,视情况为Call节点创建对应的PointsNode,然后设置逃逸状态(ConnectionGraph::add_call_node)。

    考虑到Call节点的参数们对应的PointsToNode可能还没有创建,所以add_node_to_connection_graph这一步对CallNode的处理是不完整的,需要add_final_edges来补充一下,它遍历Call节点的参数,根据BCEscapeAnalyzer的结果调整参数逃逸状态(ConnectionGraph::process_call_arguments)

    2.完成CG构造

    这一步位于complete_connection_graph。它会从逃逸状态(GlobalEscape和ArgEscape)的节点出发,标记这些节点的field节点,将它们状态也置为逃逸(我理解这一步应该对应论文里面的方法结束时的可达性分析)。除此之外,这一步还会找到那些没有逃逸的对象且其字段被初始化为null的field节点。

    到这里,CG已经建模完成了,是不是感觉漏了什么?论文中大段讨论的更新caller CG的过程好像没看到?是的,C2对与callee并不是再上一次逃逸分析,它使用之前提到的BCEscapeAnalyzer基于字节码(因为caller知道callee的字节码)做了一个保守的分析,来判断参数是ArgEscape了,还是GlobalEscape了,又或者没有逃逸。这个分析器不会产生callee CG,当然也就不需要更新caller CG了。

    3. 调整节点的标量替换状态

    4. 基于EA分析的结果优化IR

    这一步很简单,代码可以全部贴出来

    void ConnectionGraph::optimize_ideal_graph(GrowableArray<Node*>& ptr_cmp_worklist,
                                               GrowableArray<Node*>& storestore_worklist) {
      Compile* C = _compile;
      PhaseIterGVN* igvn = _igvn;
      if (EliminateLocks) {
        // Mark locks before changing ideal graph.
        int cnt = C->macro_count();
        for (int i = 0; i < cnt; i++) {
          Node *n = C->macro_node(i);
          if (n->is_AbstractLock()) { // Lock and Unlock nodes
            AbstractLockNode* alock = n->as_AbstractLock();
            if (!alock->is_non_esc_obj()) {
              if (not_global_escape(alock->obj_node())) {
                assert(!alock->is_eliminated() || alock->is_coarsened(), "sanity");
                // The lock could be marked eliminated by lock coarsening
                // code during first IGVN before EA. Replace coarsened flag
                // to eliminate all associated locks/unlocks.
                alock->set_non_esc_obj();
              }
            }
          }
        }
      }
    
      if (OptimizePtrCompare) {
        for (int i = 0; i < ptr_cmp_worklist.length(); i++) {
          Node *n = ptr_cmp_worklist.at(i);
          const TypeInt* tcmp = optimize_ptr_compare(n);
          if (tcmp->singleton()) {
            Node* cmp = igvn->makecon(tcmp);
            igvn->replace_node(n, cmp);
          }
        }
      }
    
      // For MemBarStoreStore nodes added in library_call.cpp, check
      // escape status of associated AllocateNode and optimize out
      // MemBarStoreStore node if the allocated object never escapes.
      for (int i = 0; i < storestore_worklist.length(); i++) {
        Node* storestore = storestore_worklist.at(i);
        assert(storestore->is_MemBarStoreStore(), "");
        Node* alloc = storestore->in(MemBarNode::Precedent)->in(0);
        if (alloc->is_Allocate() && not_global_escape(alloc)) {
          MemBarNode* mb = MemBarNode::make(C, Op_MemBarCPUOrder, Compile::AliasIdxBot);
          mb->init_req(TypeFunc::Memory,  storestore->in(TypeFunc::Memory));
          mb->init_req(TypeFunc::Control, storestore->in(TypeFunc::Control));
          igvn->register_new_node_with_optimizer(mb);
          igvn->replace_node(storestore, mb);
        }
      }
    }
    

    做了三个优化

    1. 如果有lock,unlock节点,且它们没有GlobalEscape,那么锁可以消除。
    2. 优化指针比较为常量
    3. 如果有StoreStore的membar,但是它们关联的分配操作没有GlobalEscape,那么这些分配没有逃逸出线程,可以将StoreStore优化为MemBarCPUOrder

    5. 精确化内存节点的类型范围

    to write

  • 相关阅读:
    【Java】XML文件的解析
    PE知识复习之PE合并节
    PE知识复习之PE的重定位表
    PE知识复习之PE的两种状态
    PE知识复习之PE的节表
    PE知识复习之PE的各种头属性解析
    PE知识复习之PE的导入表
    PE知识复习之PE的导出表
    PE知识复习之PE的绑定导入表
    第三讲扩展,VA,RVA,FA(RAW),模块地址的概念
  • 原文地址:https://www.cnblogs.com/kelthuzadx/p/15718538.html
Copyright © 2020-2023  润新知