LLVM筆記(11) - 指令選擇(三) combine


本節將要介紹指令選擇中combine優化的概念, combine的目的是簡化DAG, 合並/消除冗余節點, 為生成更優的指令做准備. 大部分combine是與架構無關的優化, 但LLVM也提供了修改combine的custom接口.
盡管本節介紹的是combine的流程, 但combine與legalize及lowering存在關聯, 我們在介紹時也會涉及相關的概念.

理解debug信息

使用上節提到的圖形化方式來閱讀DAG固然利於理解, 但是卻不方便調試. 這里更推薦使用LLVM的日志系統打印文字版的DAG描述. 在編譯時添加-mllvm -debug-only=isel即可打印SelectionDAGISel模塊的調試信息, 以下截取部分.

Initial selection DAG: %bb.0 'test:entry'
SelectionDAG has 29 nodes:
  t0: ch = EntryToken
  t2: i32,ch = CopyFromReg t0, Register:i32 %0
  t12: i32 = Constant<0>
  t14: i32,ch = load<(load 4 from %ir.p1, !tbaa !2)> t0, t2, undef:i32
      t6: i32,ch = CopyFromReg t0, Register:i32 %2
    t18: i32 = mul nsw t14, t6
            t8: i32,ch = CopyFromReg t0, Register:i32 %3
          t10: i32 = AssertZext t8, ValueType:ch:i8
        t11: i8 = truncate t10
      t16: i8 = and t11, Constant:i8<31>
    t17: i32 = zero_extend t16
  t19: i32 = shl t18, t17
  t24: i32 = GlobalAddress<i32 (i32)* @test2> 0
          t4: i32,ch = CopyFromReg t0, Register:i32 %1
        t21: i32 = shl t4, Constant:i32<2>
      t22: i32 = add t2, t21
    t23: ch = store<(store 4 into %ir.arrayidx1, !tbaa !2)> t14:1, t19, t22, undef:i32
  t26: ch,glue = CopyToReg t23, Register:i32 $x10, t19
  t28: ch,glue = RISCVISD::TAIL t26, TargetGlobalAddress:i32<i32 (i32)* @test2> 0 [TF=1], Register:i32 $x10, t26:1


Optimized lowered selection DAG: %bb.0 'test:entry'
SelectionDAG has 25 nodes:
  t0: ch = EntryToken
  t2: i32,ch = CopyFromReg t0, Register:i32 %0
  t14: i32,ch = load<(load 4 from %ir.p1, !tbaa !2)> t0, t2, undef:i32
      t6: i32,ch = CopyFromReg t0, Register:i32 %2
    t18: i32 = mul nsw t14, t6
        t8: i32,ch = CopyFromReg t0, Register:i32 %3
      t10: i32 = AssertZext t8, ValueType:ch:i8
    t30: i32 = and t10, Constant:i32<31>
  t19: i32 = shl t18, t30
          t4: i32,ch = CopyFromReg t0, Register:i32 %1
        t21: i32 = shl t4, Constant:i32<2>
      t22: i32 = add t2, t21
    t23: ch = store<(store 4 into %ir.arrayidx1, !tbaa !2)> t14:1, t19, t22, undef:i32
  t26: ch,glue = CopyToReg t23, Register:i32 $x10, t19
  t28: ch,glue = RISCVISD::TAIL t26, TargetGlobalAddress:i32<i32 (i32)* @test2> 0 [TF=1], Register:i32 $x10, t26:1

SelectionDAG類實現了一個名為dump()(defined in lib/CodeGen/SelectionDAG/SelectionDAGDumper.cpp)的接口用於打印DAG圖. 關於接口的具體實現不再贅述, 這里介紹下怎么閱讀文字版的DAG.

  1. 第一行指示了打印這個DAG的時間點(是在剛建立完DAG還是做完combine后?), 不熟悉的讀者可以直接在代碼中搜索字符串. 另外它也指示了當前處理的函數名及對應的basic block.
  2. 第二行指示了當前DAG包含的節點總數.
  3. 從第三行起, 每一行代表一個SDNode, 打印順序從上往下按照節點的persistentId順序排列(比如上文中的t0->t2->t14), 注意如果節點太簡單, 沒有輸入操作數(比如一個代表常量的ConstantSDNode), 那么它會被inline到使用它的節點中.
  4. 注意到有些節點(如t6)並未按順序排列且這類節點會有更多的縮進. 這表明了這個節點只有一個use且其user為它后面的第一個縮進比它少一級的節點. e.g. t8的唯一user是t10, 所以t10在t8后且縮進比t8少一級, 而t10的唯一user是t30, 所以t30在t10后且縮進又比t10少一級. 之所以逆序打印的原因是DAG基於樹匹配的方式做指令選擇, 后面我們將會看到這么打印利於分析指令選擇的結果及發現指令選擇的錯誤.

DAG combine的流程

SelectionDAGISel::CodeGenAndEmitDAG()中會交替調用SelectionDAG::Combine(), SelectionDAG::Legalize()等接口(即概述中的那張流程圖). 多次Combine()的原因是每次legalize后DAG都可能發生變化, 所以需要嘗試多次優化DAG. SelectionDAG::Combine()會創建一個DAGCombiner的類並調用DAGCombiner::Run(). 注意Combine會傳入一個CombineLevel(defined in include/llvm/CodeGen/DAGCombine.h)的枚舉表明調用Combine()接口時的時間點, 對應不同時間點DAGCombiner的優化也不同.

void DAGCombiner::Run(CombineLevel AtLevel) {
  ......

  // 將DAG中所有節點加入worklist
  for (SDNode &Node : DAG.allnodes())
    AddToWorklist(&Node);

  // 創建一個引用root的dummy節點來防止root節點被優化
  HandleSDNode Dummy(DAG.getRoot());

  while (SDNode *N = getNextWorklistEntry()) {
    // 如果一個節點沒有user則刪除該節點, 當一個節點被刪除時遞歸檢查它的operand是否也可以被刪除
    if (recursivelyDeleteUnusedNodes(N))
      continue;

    WorklistRemover DeadNodes(*this);

    // 如果在legalize operation之后combine節點, 必須要保證combine后的節點也是legalize的
    if (Level == AfterLegalizeDAG) {
      SmallSetVector<SDNode *, 16> UpdatedNodes;
      bool NIsValid = DAG.LegalizeOp(N, UpdatedNodes);

      for (SDNode *LN : UpdatedNodes) {
        AddUsersToWorklist(LN);
        AddToWorklist(LN);
      }
      if (!NIsValid)
        continue;
    }

    // 檢查被combine的節點的operand是否需要被加入嘗試合並/替換當前節點來化簡DAG.worklist
    CombinedNodes.insert(N);
    for (const SDValue &ChildN : N->op_values())
      if (!CombinedNodes.count(ChildN.getNode()))
        AddToWorklist(ChildN.getNode());

    // combine入口
    SDValue RV = combine(N);

    // 返回SDNode指針為空說明combine()沒有生成新的節點, 跳過替換步驟
    if (!RV.getNode())
      continue;

    // 返回的SDNode指針為原節點表明CombineTo()已經處理了該節點, 無需再次替換
    if (RV.getNode() == N)
      continue;

    // 檢查新節點返回的值的個數, 並替換舊結點
    if (N->getNumValues() == RV.getNode()->getNumValues())
      DAG.ReplaceAllUsesWith(N, RV.getNode());
    else {
      assert(N->getValueType(0) == RV.getValueType() &&
             N->getNumValues() == 1 && "Type mismatch");
      DAG.ReplaceAllUsesWith(N, &RV);
    }

    // 將新節點及其user都加入worklist
    AddToWorklist(RV.getNode());
    AddUsersToWorklist(RV.getNode());

    // 如果節點沒有user那么遞歸的刪除被替換的節點
    recursivelyDeleteUnusedNodes(N);
  }

  // 清理DAG
  DAG.setRoot(Dummy.getValue());
  DAG.RemoveDeadNodes();
}

// 將N加入到worklist的尾部, 保證DFS順序
void AddToWorklist(SDNode *N) {
  // 跳過dummy node
  if (N->getOpcode() == ISD::HANDLENODE)
    return;

  ConsiderForPruning(N);

  // 返回true表示插入成功, false表示已在map里
  if (WorklistMap.insert(std::make_pair(N, Worklist.size())).second)
    Worklist.push_back(N);
}

bool DAGCombiner::recursivelyDeleteUnusedNodes(SDNode *N) {
  // user非空, 不能刪除該節點
  if (!N->use_empty())
    return false;

  SmallSetVector<SDNode *, 16> Nodes;
  Nodes.insert(N);
  do {
    N = Nodes.pop_back_val();
    if (!N)
      continue;

    // user為空, 刪除節點同時將其從worklist中移除
    if (N->use_empty()) {
      for (const SDValue &ChildN : N->op_values())
        Nodes.insert(ChildN.getNode());

      removeFromWorklist(N);
      DAG.DeleteNode(N);
    } else {
      // user非空, 說明其一個user已被刪除, 可以嘗試再次combine
      AddToWorklist(N);
    }
  } while (!Nodes.empty());
  return true;
}

DAGCombiner::Run()的算法在legalize與combine中經常見到, 其思路是DFS遍歷DAG中的節點. 首先遍歷DAG將所有節點加入容器worklist中. 然后每次取出一個節點, 依次判斷:

  1. 該節點的user是否為空, 為空則刪除該節點. 在刪除該節點時會遞歸的判斷其引用的operand是否也可被刪除.
  2. 如果combine發生在legalize operation之后需要判斷該節點的操作是否legal. 由於combine可能產生架構不支持的節點操作, 所以需要先對取出的節點嘗試legalize operation. 那么問題來了, 是否會出現combine的邏輯是legalize的逆邏輯, 導致combine出非法節點, 而非法節點再次被legalize為原本的節點導致死循環呢?
  3. 檢查該節點的operand是否在worklist中, 若不在則將其加入worklist, 這中情況一般發生在combine出新的節點后對新的節點做combine.
  4. 調用combine()簡化該節點. 若返回的節點為空表明沒有生成新節點來替換原節點, 若返回的節點為原節點表明該節點dead(沒有user)且已被替換.
  5. 使用返回的新節點替換原節點, 必須保證新舊節點的value個數與類型一致. 然后將新節點及其user加入worklist, 最后嘗試刪除舊結點(如果沒有user).

combine()實現如下:

SDValue DAGCombiner::combine(SDNode *N) {
  // 架構無關優化
  SDValue RV = visit(N);

  // 架構相關優化
  if (!RV.getNode()) {
    if (N->getOpcode() >= ISD::BUILTIN_OP_END ||
        TLI.hasTargetDAGCombine((ISD::NodeType)N->getOpcode())) {

      TargetLowering::DAGCombinerInfo
        DagCombineInfo(DAG, Level, false, this);

      RV = TLI.PerformDAGCombine(N, DagCombineInfo);
    }
  }

  // promote operation
  if (!RV.getNode()) {
    switch (N->getOpcode()) {
    default: break;
    case ISD::ADD:
    case ISD::SUB:
    case ISD::MUL:
    case ISD::AND:
    case ISD::OR:
    case ISD::XOR:
      RV = PromoteIntBinOp(SDValue(N, 0));
      break;
    case ISD::SHL:
    case ISD::SRA:
    case ISD::SRL:
      RV = PromoteIntShiftOp(SDValue(N, 0));
      break;
    case ISD::SIGN_EXTEND:
    case ISD::ZERO_EXTEND:
    case ISD::ANY_EXTEND:
      RV = PromoteExtend(SDValue(N, 0));
      break;
    case ISD::LOAD:
      if (PromoteLoad(SDValue(N, 0)))
        RV = SDValue(N, 0);
      break;
    }
  }

  ......

  return RV;
}

combine()的邏輯分為三部分:

  1. target independent combine. 架構無關的combine包含了絕大多數優化, 類似IR中的InstCombine, 通過運算來化簡操作. 以AND為例, visitAND()中會嘗試優化一下運算: x & x -> x, x & 0 -> 0, x & -1 -> x. 關於架構無關的combine分析, 可以參考DAGCombiner::visit()實現, 這里不再贅述.
  2. target dependent combine. 架構相關的combine比較少見, 主要是為了利用架構指令集特點. 比如如果一個架構的mul操作cost較低, 那公共架構將mul轉成shift與or操作就顯得不太合理, 那么可以在target dependent combine中轉換為mul. 另外架構無關的combine只能處理架構無關的操作(ISDOpcode), 對於自定義的架構相關的操作需要在此處理. 對於前者的情況, 需要首先調用TargetLoweringBase::setTargetDAGCombine()(TargetLoweringBase是基類, 需要在對應的[arch]TargetLowering中override該接口)設置需要自定義combine的架構無關的SDNode, 並且在TargetLoweringBase::PerformDAGCombine()中實現自定義的combine方式.
  3. promote operation. 最后一種promote操作與以后將要介紹的legalize中的promote有些類似, 指的是對同一操作的不同類型的操作數, 嘗試將其轉換為其它類型的操作數. 比如在X86上同時支持16bit加法與32bit加法, 但是16bit加法的指令字長比32bit更長, 因此可以使用32bit加法來替換16bit加法. 注意這里promote與legalize中promote的區別: legalize中的promote行為是必須的, 即由於架構不支持某個操作數類型, legalize才做對應的promote操作. 而combine中的操作都是合法的, 只是combine后的操作比combine前的更優. 在自定義架構上修改代碼時需要注意這個區別.

最后我們看下target dependent combine的實現, 上文提到的數據結構如下.

class TargetLoweringBase {
  // 記錄是否需要target comhbine的target independent的Node的標記位數組
  unsigned char TargetDAGCombineArray[(ISD::BUILTIN_OP_END+CHAR_BIT-1)/CHAR_BIT];

public:
  bool hasTargetDAGCombine(ISD::NodeType NT) const {
    assert(unsigned(NT >> 3) < array_lengthof(TargetDAGCombineArray));
    return TargetDAGCombineArray[NT >> 3] & (1 << (NT&7));
  }

  void setTargetDAGCombine(ISD::NodeType NT) {
    assert(unsigned(NT >> 3) < array_lengthof(TargetDAGCombineArray));
    TargetDAGCombineArray[NT >> 3] |= 1 << (NT&7);
  }

  // 注意該接口的返回值與架構無關的combine實現(visit())是一致的:
  // 返回空的SDValue代表沒有修改, 返回指向入參的SDValue代表節點已被替換, 否則在combine()中被替換
  virtual SDValue PerformDAGCombine(SDNode *N, DAGCombinerInfo &DCI) const;
};

以X86為例, X86TargetLowering::X86TargetLowering()中自定義了許多架構無關的Node. 我們看一個LOAD的例子.

static SDValue combineLoad(SDNode *N, SelectionDAG &DAG,
                           TargetLowering::DAGCombinerInfo &DCI,
                           const X86Subtarget &Subtarget) {
  LoadSDNode *Ld = cast<LoadSDNode>(N);
  EVT RegVT = Ld->getValueType(0);
  EVT MemVT = Ld->getMemoryVT();
  SDLoc dl(Ld);
  const TargetLowering &TLI = DAG.getTargetLoweringInfo();

  // 對於32byte非對齊load性能較差的架構, 使用2次16byte的load來提升性能
  // fast返回架構是否支持正常的32byte非對齊laod
  ISD::LoadExtType Ext = Ld->getExtensionType();
  bool Fast;
  unsigned Alignment = Ld->getAlignment();
  if (RegVT.is256BitVector() && !DCI.isBeforeLegalizeOps() &&
      Ext == ISD::NON_EXTLOAD &&
      ((Ld->isNonTemporal() && !Subtarget.hasInt256() && Alignment >= 16) ||
       (TLI.allowsMemoryAccess(*DAG.getContext(), DAG.getDataLayout(), RegVT,
                               *Ld->getMemOperand(), &Fast) &&
        !Fast))) {
    unsigned NumElems = RegVT.getVectorNumElements();
    if (NumElems < 2)
      return SDValue();

    unsigned HalfAlign = 16;
    SDValue Ptr1 = Ld->getBasePtr();
    SDValue Ptr2 = DAG.getMemBasePlusOffset(Ptr1, HalfAlign, dl);
    EVT HalfVT = EVT::getVectorVT(*DAG.getContext(), MemVT.getScalarType(),
                                  NumElems / 2);
    // 拆分為兩條load
    SDValue Load1 =
        DAG.getLoad(HalfVT, dl, Ld->getChain(), Ptr1, Ld->getPointerInfo(),
                    Alignment, Ld->getMemOperand()->getFlags());
    SDValue Load2 = DAG.getLoad(HalfVT, dl, Ld->getChain(), Ptr2,
                                Ld->getPointerInfo().getWithOffset(HalfAlign),
                                MinAlign(Alignment, HalfAlign),
                                Ld->getMemOperand()->getFlags());
    // 建立依賴
    SDValue TF = DAG.getNode(ISD::TokenFactor, dl, MVT::Other,
                             Load1.getValue(1), Load2.getValue(1));

    // 合並兩個load結果
    SDValue NewVec = DAG.getNode(ISD::CONCAT_VECTORS, dl, RegVT, Load1, Load2);
    // 調用CombineTo()替換原來的Node
    return DCI.CombineTo(N, NewVec, TF, true);
  }

  if (Ext == ISD::NON_EXTLOAD && !Subtarget.hasAVX512() && RegVT.isVector() &&
      RegVT.getScalarType() == MVT::i1 && DCI.isBeforeLegalize()) {
    unsigned NumElts = RegVT.getVectorNumElements();
    EVT IntVT = EVT::getIntegerVT(*DAG.getContext(), NumElts);
    if (TLI.isTypeLegal(IntVT)) {
      SDValue IntLoad = DAG.getLoad(IntVT, dl, Ld->getChain(), Ld->getBasePtr(),
                                    Ld->getPointerInfo(), Alignment,
                                    Ld->getMemOperand()->getFlags());
      SDValue BoolVec = DAG.getBitcast(RegVT, IntLoad);
      return DCI.CombineTo(N, BoolVec, IntLoad.getValue(1), true);
    }
  }

  return SDValue();
}

小結:

  1. combine是用來簡化DAG的優化, 它主要分成三塊, 架構無關的combine, 架構相關的combine以及promote operation.
  2. 架構無關的優化通常是通過簡化算術運算實現, 同時也會考慮指令的cost(mul/div的cost肯定比bit operation來的大).
  3. 架構相關的優化通常與具體的硬件綁定, 一般在[arch]TargetLowering類中覆寫基類的回調, 找不到接口時可以去TargetLowering.h中查找.
  4. promote operation類似legalize中的promote, 是通過轉換數據類型來實現優化. 它與legalize中promote的最大區別是combine中前后行為都是合法的, 只是性能不同.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM