LLVM筆記(13) - 指令選擇(五) select


本節主要介紹指令選擇的具體步驟(select). select是將基於ISD SDNode的DAG替換成基於機器指令節點的DAG的過程.

select基本流程

在完成combine與legalize之后SelectionDAGISel::CodeGenAndEmitDAG()會調用SelectionDAGISel::DoInstructionSelection()進行指令選擇.

void SelectionDAGISel::DoInstructionSelection() {
  PreprocessISelDAG();

  {
    DAGSize = CurDAG->AssignTopologicalOrder();

    HandleSDNode Dummy(CurDAG->getRoot());
    SelectionDAG::allnodes_iterator ISelPosition (CurDAG->getRoot().getNode());
    ++ISelPosition;

    ISelUpdater ISU(*CurDAG, ISelPosition);

    while (ISelPosition != CurDAG->allnodes_begin()) {
      SDNode *Node = &*--ISelPosition;
      if (Node->use_empty())
        continue;

      Select(Node);
    }

    CurDAG->setRoot(Dummy.getValue());
  }

  PostprocessISelDAG();
}

PreprocessISelDAG()與PostprocessISelDAG()是SelectionDAGISel類提供的hook, 用於指令選擇前后做架構相關的優化. 在這之前幾個架構custom的時間點中DAG combine時不能保證opcode與operand的合法性, custom legalize聚焦於opcode的合法化以保證指令選擇時覆蓋整個DAG的節點, 而此處兩個接口最適合做基於(合法化后的)DAG的窺孔優化.
注意到在指令選擇前還會調用一次AssignTopologicalOrder()重新排序, 並且以逆序方式遍歷節點, 這種從上往下的遍歷方式與SelectCode代碼邏輯結合, 保證了最貪婪的tree pattern匹配.
Select()又是一個SelectionDAGISel類提供的hook, 每個后端架構都必須要實現該接口, 仍然以RISCV架構為例.

void RISCVDAGToDAGISel::Select(SDNode *Node) {
  if (Node->isMachineOpcode()) {
    Node->setNodeId(-1);
    return;
  }

  // Instruction Selection not handled by the auto-generated tablegen selection
  // should be handled here.
  unsigned Opcode = Node->getOpcode();
  MVT XLenVT = Subtarget->getXLenVT();
  SDLoc DL(Node);
  EVT VT = Node->getValueType(0);

  switch (Opcode) {
  case ISD::Constant: {
    auto ConstNode = cast<ConstantSDNode>(Node);
    if (VT == XLenVT && ConstNode->isNullValue()) {
      SDValue New = CurDAG->getCopyFromReg(CurDAG->getEntryNode(), SDLoc(Node),
                                           RISCV::X0, XLenVT);
      ReplaceNode(Node, New.getNode());
      return;
    }
    int64_t Imm = ConstNode->getSExtValue();
    if (XLenVT == MVT::i64) {
      ReplaceNode(Node, selectImm(CurDAG, SDLoc(Node), Imm, XLenVT));
      return;
    }
    break;
  }

  ......
  }

  // Select the default instruction.
  SelectCode(Node);
}

static SDNode *selectImm(SelectionDAG *CurDAG, const SDLoc &DL, int64_t Imm,
                         MVT XLenVT) {
  RISCVMatInt::InstSeq Seq;
  RISCVMatInt::generateInstSeq(Imm, XLenVT == MVT::i64, Seq);

  SDNode *Result = nullptr;
  SDValue SrcReg = CurDAG->getRegister(RISCV::X0, XLenVT);
  for (RISCVMatInt::Inst &Inst : Seq) {
    SDValue SDImm = CurDAG->getTargetConstant(Inst.Imm, DL, XLenVT);
    if (Inst.Opc == RISCV::LUI)
      Result = CurDAG->getMachineNode(RISCV::LUI, DL, XLenVT, SDImm);
    else
      Result = CurDAG->getMachineNode(Inst.Opc, DL, XLenVT, SrcReg, SDImm);

    // Only the first instruction has X0 as its source.
    SrcReg = SDValue(Result, 0);
  }

  return Result;
}

通常一個后端架構的Select()接口由兩部分組成, 手工編碼指令選擇與通過td自動生成pattern match的匹配代碼, 通常不能通過編寫pattern獲取最佳選擇的指令會通過手工編碼的方式, 因此這塊代碼在pattern match代碼前執行. 只有在手工選擇失敗之后才會調用SelectCode()進行pattern match匹配, 如果還失敗則會調用SelectionDAGISel::CannotYetSelect()報錯(在移植一個新架構時常見情況).
我們以RISCV為例看下什么情況需要手工編碼指令選擇. 其常量賦值(給寄存器)的語義對應的硬件指令比較特殊, 原因是

  1. RISCV的通用寄存器包含像ARM一樣的zero寄存器X0, 立即數0可以直接lowering為X0寄存器.
  2. RISCV標准指令集沒有32bit立即數賦值指令, 對小於4K的立即數可以使用addi指令賦值, 對於低12bit全0的立即數可以單獨使用lui指令賦值, 否則需要lui + addi指令組合賦值.

可見Select()中首先對ISD::Constant節點判斷其值是否為0, 為0則直接替換為寄存器拷貝, 其次調用selectImm()中對第二種情況分類討論.
在選擇了最優的指令后調用SelectionDAG::getMachineNode()創建一個新節點, 注意該接口與SelectionDAG::getNode()的區別在於對glue節點處理的不同, 其它處理是一樣的.

MachineSDNode *SelectionDAG::getMachineNode(unsigned Opcode, const SDLoc &DL,
                                            SDVTList VTs,
                                            ArrayRef<SDValue> Ops) {
  bool DoCSE = VTs.VTs[VTs.NumVTs-1] != MVT::Glue;
  MachineSDNode *N;
  void *IP = nullptr;

  if (DoCSE) {
    FoldingSetNodeID ID;
    AddNodeIDNode(ID, ~Opcode, VTs, Ops);
    IP = nullptr;
    if (SDNode *E = FindNodeOrInsertPos(ID, DL, IP)) {
      return cast<MachineSDNode>(UpdateSDLocOnMergeSDNode(E, DL));
    }
  }

  // Allocate a new MachineSDNode.
  N = newSDNode<MachineSDNode>(~Opcode, DL.getIROrder(), DL.getDebugLoc(), VTs);
  createOperands(N, Ops);

  if (DoCSE)
    CSEMap.InsertNode(N, IP);

  InsertNode(N);
  NewSDValueDbgMsg(SDValue(N, 0), "Creating new machine node: ", this);
  return N;
}

SDValue SelectionDAG::getNode(unsigned Opcode, const SDLoc &DL, EVT VT) {
  FoldingSetNodeID ID;
  AddNodeIDNode(ID, Opcode, getVTList(VT), None);
  void *IP = nullptr;
  if (SDNode *E = FindNodeOrInsertPos(ID, DL, IP))
    return SDValue(E, 0);

  auto *N = newSDNode<SDNode>(Opcode, DL.getIROrder(), DL.getDebugLoc(),
                              getVTList(VT));
  CSEMap.InsertNode(N, IP);

  InsertNode(N);
  SDValue V = SDValue(N, 0);
  NewSDValueDbgMsg(V, "Creating new node: ", this);
  return V;
}

基於pattern match指令選擇實現

SelectCode()是tablegen自動生成的指令選擇器的實現, 其定義見llvm build目錄下lib/Target/[arch]/[arch]GenDAGISel.inc(其中arch為具體后端架構名). 以RISCV為例其實現如下

void DAGISEL_CLASS_COLONCOLON SelectCode(SDNode *N)
{
  // Some target values are emitted as 2 bytes, TARGET_VAL handles
  // this.
  #define TARGET_VAL(X) X & 255, unsigned(X) >> 8
  static const unsigned char MatcherTable[] = {
  ......
  }; // Total Array size is 25735 bytes

  #undef TARGET_VAL
  SelectCodeCommon(N, MatcherTable,sizeof(MatcherTable));
}

注意到SelectCode()定義了一個非常大的靜態數組並將其傳給SelectCodeCommon, 該數組是tablegen根據td中定義的pattern生成的一個狀態跳轉表, 我們將在接下來解釋其具體構成.
在這之前我們首先思考一個問題, 給定指令的語義與輸入DAG, 如何將其映射到對應的指令? 舉個簡單的例子一個架構支持三條指令, MUL對應乘法運算, SUB對應減法運算, SRL對應邏輯右移, 那么對應下圖1中的DAG我們該如何將其映射為機器指令.

最簡單的方式是將指令描述為對應的DAG節點, 然后通過展開的方式一一匹配. 如圖2中我們將DAG中每個操作節點分割開來, 然后一一匹配到對應的機器指令.
但是這種方式帶來的效率的問題: 選擇的指令不一定是最優的. 如果這個架構又支持了一條MS指令, 其作用是將前兩個操作數相乘后再邏輯右移, 宏展開的方式無法選擇恰當的指令. 對此我們使用DAG covering的方式, 將DAG划分為一個個子圖, 對每個子圖做檢查, 查看指令的語義是否能匹配子圖, 如果可以則替換為機器指令, 否則將子圖划分為更小的子圖遞歸檢查.
如圖3所示, 首先從root節點開始搜索, 根據操作符確定候選的SUB/MS兩條指令, 再向下搜索第一個來源mul確定候選的MS指令, 依次類推. 再候選過程中可能存在一個DAG對應多個子圖划分的情況(比如上文也可以划分為mul一個子圖和另一個子圖add + sub, 如果架構支持一個指令將add的結果減去另一個值), 在權衡不同划分的優劣時可以使用動態規划來求解.
更進一步的研究建議閱讀Survey on Instruction Selection第四章, 本文將聚焦於LLVM的實現.
LLVM的實現與上文描述中稍微有些區別:

  1. 指令語義是以tree pattern的方式描述的. tree與DAG最大區別在於tree中的節點只有一個輸出邊, 這樣大大簡化了子圖的復雜度. 缺點是由於被選擇的DAG中存在一個節點被多個節點引用的情況(多條輸出邊), 對於這類情況需要將其轉換為tree形式(這步被稱為undagging).
  2. LLVM使用貪婪的匹配策略, 因此選擇結果不一定是全局最優的.

LLVM使用tablegen處理td中描述的pattern, 將其分類並設定優先級, 其結果轉化成一段特殊格式的數組(也被稱作匹配表). 我們截取一段RISCV的匹配表作為例子來解釋其含義與構成.

  static const unsigned char MatcherTable[] = {
/*     0*/  OPC_SwitchOpcode /*81 cases */, 12|128,5/*652*/, TARGET_VAL(ISD::AND),// ->657
/*     5*/    OPC_Scope, 41|128,4/*553*/, /*->561*/ // 2 children in Scope
/*     8*/      OPC_CheckAndImm, 127|128,127|128,127|128,127|128,15/*4294967295*/, 
/*    14*/      OPC_Scope, 75|128,3/*459*/, /*->476*/ // 2 children in Scope
/*    17*/        OPC_MoveChild0,
/*    18*/        OPC_SwitchOpcode /*2 cases */, 96|128,1/*224*/, TARGET_VAL(RISCVISD::DIVUW),// ->247
/*    23*/          OPC_MoveChild0,
/*    24*/          OPC_Scope, 110, /*->136*/ // 2 children in Scope
/*    26*/            OPC_CheckAndImm, 127|128,127|128,127|128,127|128,15/*4294967295*/, 
/*    32*/            OPC_RecordChild0, // #0 = $rs1
/*    33*/            OPC_MoveParent,
/*    34*/            OPC_MoveChild1,
/*    35*/            OPC_Scope, 49, /*->86*/ // 2 children in Scope
/*    37*/              OPC_CheckAndImm, 127|128,127|128,127|128,127|128,15/*4294967295*/, 
/*    43*/              OPC_RecordChild0, // #1 = $rs2
/*    44*/              OPC_MoveParent,
/*    45*/              OPC_MoveParent,
/*    46*/              OPC_SwitchType /*2 cases */, 24, MVT::i32,// ->73
/*    49*/                OPC_Scope, 10, /*->61*/ // 2 children in Scope
/*    51*/                  OPC_CheckPatternPredicate, 0, // (Subtarget->hasStdExtM()) && (Subtarget->is64Bit()) && (MF->getSubtarget().checkFeatures("-64bit"))
/*    53*/                  OPC_MorphNodeTo1, TARGET_VAL(RISCV::DIVU), 0,
                                MVT::i32, 2/*#Ops*/, 0, 1, 
                            // Src: (and:{ *:[i32] } (riscv_divuw:{ *:[i32] } (and:{ *:[i32] } GPR:{ *:[i32] }:$rs1, 4294967295:{ *:[i32] }), (and:{ *:[i32] } GPR:{ *:[i32] }:$rs2, 4294967295:{ *:[i32] })), 4294967295:{ *:[i32] }) - Complexity = 27
                            // Dst: (DIVU:{ *:[i32] } GPR:{ *:[i32] }:$rs1, GPR:{ *:[i32] }:$rs2)
/*    61*/                /*Scope*/ 10, /*->72*/
/*    62*/                  OPC_CheckPatternPredicate, 1, // (Subtarget->hasStdExtM()) && (Subtarget->is64Bit())
/*    64*/                  OPC_MorphNodeTo1, TARGET_VAL(RISCV::DIVU), 0,
                                MVT::i32, 2/*#Ops*/, 0, 1, 
                            // Src: (and:{ *:[i32] } (riscv_divuw:{ *:[i32] } (and:{ *:[i32] } GPR:{ *:[i32] }:$rs1, 4294967295:{ *:[i32] }), (and:{ *:[i32] } GPR:{ *:[i32] }:$rs2, 4294967295:{ *:[i32] })), 4294967295:{ *:[i32] }) - Complexity = 27
                            // Dst: (DIVU:{ *:[i32] } GPR:{ *:[i32] }:$rs1, GPR:{ *:[i32] }:$rs2)
/*    72*/                0, /*End of Scope*/

  ......
  };

首先注意到這個表是以unsigned char型數組方式編碼的, 每行起始的注釋指示行首元素在數組中的下標, 方便我們快速查找.
每行行首的元素是以OPC_起始的枚舉值(其值見SelectionDAGISel::BuiltinOpcodes定義), 指示了匹配表要跳轉的下一狀態. 跟在狀態枚舉之后的元素的含義由狀態機根據當前狀態進行解釋, 一個狀態所需的所有信息均記錄在同一行內. 關於狀態枚舉的具體含義我們結合代碼來具體介紹.
行首注釋與狀態枚舉之間的縮進長度指示了該狀態的所屬的層級. 舉例而言對於pattern (add a, (sub b, c)), 檢查操作數b的范圍與檢查操作數c的范圍兩個狀態是平級的, 檢查操作數字a的范圍肯定優先於檢查操作數b的范圍(先匹配樹的根節點, 再葉子節點). 利用縮進可以圖形化閱讀狀態跳轉表.
表中有些特殊的立即數編碼, 比如第一行中的下標1和下標2元素(12|128,5/652/). 這么編碼的原因是因為這個表是unsigned char型數組, 而許多枚舉/下標/立即數范圍大於1個byte的大小, 因此需要變長編碼(使用定長編碼數組會大大增加表的大小, 以X86為例當前大小為500K, 如果使用int數組需要2M空間). 變長編碼的方式是取最高位指示是否需要讀入后一字節, 即12|128,5的編碼值為(12 + 5 << 7 = 652), 正好等於注釋里的值. 其解碼接口見GetVBR()(defined in lib/CodeGen/SelectionDAG/SelectionDAGISel.cpp).

LLVM_ATTRIBUTE_ALWAYS_INLINE static inline uint64_t
GetVBR(uint64_t Val, const unsigned char *MatcherTable, unsigned &Idx) {
  assert(Val >= 128 && "Not a VBR");
  Val &= 127;  // Remove first vbr bit.

  unsigned Shift = 7;
  uint64_t NextBits;
  do {
    NextBits = MatcherTable[Idx++];
    Val |= (NextBits&127) << Shift;
    Shift += 7;
  } while (NextBits & 128);

  return Val;
}

我們先回到SelectCodeCommon(), 它接受三個參數, 依次是將被選擇的節點, 根據tablegen生成的匹配表, 以及匹配表長度, 如果匹配成功則替換NodeToMatch, 否則調用CannotYetSelect()報錯退出. 代碼分為兩個部分, 第一部分是准備階段, 判斷節點的屬性, 加載匹配表, 第二部分是匹配階段, 根據載入的匹配表進行匹配.

void SelectionDAGISel::SelectCodeCommon(SDNode *NodeToMatch,
                                        const unsigned char *MatcherTable,
                                        unsigned TableSize) {
  switch (NodeToMatch->getOpcode()) {
  default:
    break;
  case ISD::EntryToken:
  case ISD::BasicBlock:
  case ISD::Register:
  case ISD::RegisterMask:
  case ISD::TargetConstant:
  case ISD::TargetConstantFP:
  case ISD::TargetConstantPool:
  case ISD::TargetFrameIndex:
  case ISD::TargetExternalSymbol:
  case ISD::MCSymbol:
    NodeToMatch->setNodeId(-1);
    return;

  ......
  }

  SmallVector<SDValue, 8> NodeStack;
  SDValue N = SDValue(NodeToMatch, 0);
  NodeStack.push_back(N);

  // 記錄當前匹配的pattern對應匹配表的位置, 如果當前匹配失敗則從對應位置繼續
  SmallVector<MatchScope, 8> MatchScopes;

  // 記錄已被匹配的節點及其對應的父節點, 根節點的父節點為空
  SmallVector<std::pair<SDValue, SDNode*>, 8> RecordedNodes;

  // 記錄已匹配的pattern的memory信息
  SmallVector<MachineMemOperand*, 2> MatchedMemRefs;

  // 記錄當前的chain與glue依賴信息
  SDValue InputChain, InputGlue;

  // 記錄被匹配的pattern的chain信息
  SmallVector<SDNode*, 3> ChainNodesMatched;

  // 記錄當前所在匹配表的位置
  unsigned MatcherIndex = 0;

  if (!OpcodeOffset.empty()) {
    // OpcodeOffset非空, 即已經加載過整張匹配表
    if (N.getOpcode() < OpcodeOffset.size())
      MatcherIndex = OpcodeOffset[N.getOpcode()];

  } else if (MatcherTable[0] == OPC_SwitchOpcode) {
    // 第一次匹配, 從頭讀入匹配表
    unsigned Idx = 1;
    while (true) {
      // CaseSize等於根節點為該Opc的pattern對應的匹配表的長度
      unsigned CaseSize = MatcherTable[Idx++];
      if (CaseSize & 128)
        CaseSize = GetVBR(CaseSize, MatcherTable, Idx);
      // 長度為0表示查找到達表的末尾
      if (CaseSize == 0) break;

      uint16_t Opc = MatcherTable[Idx++];
      Opc |= (unsigned short)MatcherTable[Idx++] << 8;
      if (Opc >= OpcodeOffset.size())
        OpcodeOffset.resize((Opc+1)*2);
      OpcodeOffset[Opc] = Idx;
      Idx += CaseSize;
    }

    // 如果找到對應的opcode則設置下標到起始位置
    if (N.getOpcode() < OpcodeOffset.size())
      MatcherIndex = OpcodeOffset[N.getOpcode()];
  }


  while (true) {
    BuiltinOpcodes Opcode = (BuiltinOpcodes)MatcherTable[MatcherIndex++];
    // 狀態機處理
    switch (Opcode) {
    ......
    }

    // 匹配成功時switch會直接continue
    // 匹配失敗時才會到此處, 嘗試回溯之前的匹配
    // 方式是首先檢查當前scope的下一元素, 直到遍歷所有child
    // 如果仍然失敗再回溯該scope的父節點
    ++NumDAGIselRetries;
    while (true) {
      // scope棧為空, 匹配失敗
      if (MatchScopes.empty()) {
        CannotYetSelect(NodeToMatch);
        return;
      }

      // 回溯最近的scope
      MatchScope &LastScope = MatchScopes.back();
      RecordedNodes.resize(LastScope.NumRecordedNodes);
      NodeStack.clear();
      NodeStack.append(LastScope.NodeStack.begin(), LastScope.NodeStack.end());
      N = NodeStack.back();

      if (LastScope.NumMatchedMemRefs != MatchedMemRefs.size())
        MatchedMemRefs.resize(LastScope.NumMatchedMemRefs);
      MatcherIndex = LastScope.FailIndex;

      InputChain = LastScope.InputChain;
      InputGlue = LastScope.InputGlue;
      if (!LastScope.HasChainNodesMatched)
        ChainNodesMatched.clear();

      // 檢查該scope的長度, 為0代表是該scope中最后一個pattern
      unsigned NumToSkip = MatcherTable[MatcherIndex++];
      if (NumToSkip & 128)
        NumToSkip = GetVBR(NumToSkip, MatcherTable, MatcherIndex);

      if (NumToSkip != 0) {
        LastScope.FailIndex = MatcherIndex+NumToSkip;
        break;
      }

      // 該scope中所有匹配失敗, 回溯父節點
      MatchScopes.pop_back();
    }
  }
}

第一步首先判斷節點類型決定是否選擇機器指令.

  1. 像AssertZext, AssertSext這類偽節點(用作編譯器內部檢測的節點)無需做指令選擇, 直接將其移除.
  2. Constant節點如果在legalize階段已被轉化為TargetConstant則表明對應指令支持立即數編碼也無需選擇.
  3. 像CopyToReg, CopyFromReg等公共框架的偽指令也無需選擇.
  4. EntryToken, TokenFactor等用做指示DAG約束關系的節點也無需選擇, 但為方便之后指令排序這里不會刪除.

以上這些指令(除第一類外)會直接設置NodeId為-1表明已被選擇.
第二步加載匹配表, 從上文介紹可以看到匹配表是一個非常大的數組, 為加速索引SelectionDAGISel類使用一個vector容器OpcodeOffset來緩存不同pattern的根節點在匹配表中的起始位置, 其下標為根節點Opcode的枚舉值. 利用這個緩存可以線性查找根節點的匹配.
在第一次調用SelectCodeCommon()時OpcodeOffset為空, 走else分支從頭加載匹配表, OPC_SwitchOpcode指示匹配表當前位置用來檢查Opcode. 以上文RISCV的匹配表為例, 匹配表第一個元素即OPC_SwitchOpcode, 其后的注釋/81 cases/代表與其同級的OPC_SwitchOpcode共有81個(即按根節點的Opcode來分類共有81種分類)(X86是428類). OPC_SwitchOpcode之后跟隨的兩個元素分別是casesize與opcode, 后者為這個OPC_SwitchOpcode對應的根節點的opcode, 前者為OPC_SwitchOpcode的管理范圍(根節點為opcode的pattern在匹配表里的長度), 用來指示當opcode匹配失敗時需要跳轉到下一個OPC_SwitchOpcode的數組下標.

通常情況下一條指令可能對應多個pattern, 一個DAG Node也可能對應多條指令, 比如(and reg, imm)與(and reg, reg)一定對應不同指令, 考慮到一些復雜pattern比如(and (mul reg, reg), imm)或者(and reg, (shl reg, imm)), 如果一一比較效率較低. 一個辦法是根據匹配的條件對這些pattern分類, 形成樹狀的結構來快速索引pattern, OPC_Scope的作用就是分類匹配條件. 假定我們當前匹配的是and節點, 接下來讀取的第一個OPC_Scope指示當前需要檢查一個匹配條件, 緊跟在它后面的值指示該scope的長度(即條件判斷失敗時下一個判斷條件的位置, 該值為41|128,4/553/), 再后面是OPC_CheckAndImm, 指示該scope中的pattern需要滿足輸入的節點的第二個操作數為立即數的條件.

    case OPC_Scope: {
      // 順序查找當前scope中滿足條件的匹配
      // 只有在找到一個匹配后再將其加入MatchScopes中
      unsigned FailIndex;

      while (true) {
        // 獲取該scope的長度(匹配失敗時跳轉到下一scope的數組下標)
        unsigned NumToSkip = MatcherTable[MatcherIndex++];
        if (NumToSkip & 128)
          NumToSkip = GetVBR(NumToSkip, MatcherTable, MatcherIndex);
        // 長度為0表明到達該scope的末尾
        if (NumToSkip == 0) {
          FailIndex = 0;
          break;
        }

        FailIndex = MatcherIndex+NumToSkip;

        unsigned MatcherIndexOfPredicate = MatcherIndex;

        // 判斷是否匹配
        bool Result;
        MatcherIndex = IsPredicateKnownToFail(MatcherTable, MatcherIndex, N,
                                              Result, *this, RecordedNodes);
        // 成功則跳出循環
        if (!Result)
          break;

        ++NumDAGIselRetries;

        // 否則檢查下一scope
        MatcherIndex = FailIndex;
      }

      // If the whole scope failed to match, bail.
      if (FailIndex == 0) break;

      // 記錄當前scope的位置, 若子節點匹配失敗時需要從該位置回溯
      MatchScope NewEntry;
      NewEntry.FailIndex = FailIndex;
      NewEntry.NodeStack.append(NodeStack.begin(), NodeStack.end());
      NewEntry.NumRecordedNodes = RecordedNodes.size();
      NewEntry.NumMatchedMemRefs = MatchedMemRefs.size();
      NewEntry.InputChain = InputChain;
      NewEntry.InputGlue = InputGlue;
      NewEntry.HasChainNodesMatched = !ChainNodesMatched.empty();
      MatchScopes.push_back(NewEntry);
      continue;
    }

由於匹配表是順序查找的, 一開始匹配到的pattern不一定是最終的pattern, 因此我們需要記錄每次匹配的位置, 當匹配失敗時可以從對應的位置重新開始匹配, 這個信息存放在MatchScopes中.
回到SelectCodeCommon(), 匹配成功時switch會continue跳過剩余代碼. 而匹配失敗時會檢查MatchScopes是否非空, 若非空則取出最后一個節點重新開始匹配. 如果scope對應的所有元素都匹配失敗則返回父節點重新開始匹配, 直到匹配成功或者MatchScopes為空.

到這匹配表的構成基本上已經解釋的七七八八了, 現在我們可以回頭看下OPC_*枚舉, 其大致分類如下.

  1. OPC_SwitchOpcode 指示匹配的opcode分類
  2. OPC_Scope 指示匹配的操作數的條件分類
  3. OPC_Record* 記錄當前匹配成功的節點, 在生成MachineNode時會被替換掉(如果節點沒有其它引用的話)
  4. OPC_Move* 記錄當前匹配的操作數(被機器指令使用的操作數, 復雜pattern中的中間結果不會記錄), 創建MachineNode時用到
  5. OPC_Check* 判斷是否滿足匹配條件
  6. OPC_Emit* 匹配成功, 創建MachineNode並替換原有的DAG Node
  7. OPC_Morph* 類似OPC_Emit*, 區別在於被morph的節點可能有其它引用, 需要額外檢查來保證是否刪除

遺留問題:

  1. 既然匹配表是順序檢索的, 那么一個DAG如果對應兩條pattern, 后面的pattern不是永遠選不到? 是的, 因此LLVM為pattern設置了優先級. 具體而言
    a. 后端td中用於描述pattern的Pattern類(defined in include/llvm/Target/TargetSelectionDAG.td)有個成員AddedComplexity, 編譯器開發人員可以通過設置該值調整指令選擇的優先級. 類似的對於ComplexPattern類有個成員Complexity也可以設置ComplexPattern的優先級.
    b. tablegen后端會根據pattern的形式計算其complexity, 再加上額外設置的值得到最終值, 生成的匹配表按最終值進行排序, 相同優先級指令根據cost(指令數)排序. pattern本身的complexity計算比價復雜, 簡要而言對於復雜pattern逐個計算然后計算總值, 如果沒有復雜pattern則根據操作數個數計算, 對於操作數是立即數的會有額外加成(否則需要額外指令將立即數移動到寄存器). 具體的計算方式以后有空再分析.
  2. 兩類特殊的依賴節點chain與glue的處理.
    TODO
  3. 補張圖方便理解.


免責聲明!

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



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