LLVM筆記(18) - IR基礎詳解(二) Instruction


上節介紹了IR中底層的數據表達方式(Value)及其組織方式(有向圖), 有了這個基礎就可以理解LLVM IR中的一些基本構成. 本節將要介紹其中的一個基礎概念: 指令(Instruction).

LLVM IR指令基礎

LLVM使用Instruction來描述一條具體的指令. 與ISA設計類似, 在LLVM中指令可以細分為若干類:

  1. 算術與位運算指令: 絕大部分指令可以歸屬為一元操作數指令(UnaryInstruction)與二元操作數指令(BinaryInstruction).
  2. 控制流指令: 包含(函數內)跳轉指令(branch/indirect branch), 函數調用與返回(call/invoke/ret), 分支跳轉(switch)等改變控制流的指令.
  3. 訪存指令: 操作內存的指令(load/store).
  4. 其它指令: 不同於常見ISA的特有指令, 比如比較指令(icmp), 選擇賦值指令(select), 以及維護SSA的必需品phi指令.

關於更多的IR指令介紹可以參見官方文檔. 本文假定讀者已經對IR有了基礎了解, 將更多注重代碼實現細節.
所有的指令都從Instruction類繼承而來, 不同分類的指令有不同的特性. 我們首先看下其基礎結構與實現邏輯, 再針對性介紹幾類指令的細節.

通用指令定義

LLVM使用Instruction(defined in include/llvm/IR/Instruction.h)類來描述一條通用指令, 這里的通用的含義是所有指令都具有的行為(比如操作數信息), 具體而言包括:

  1. 指令類型.
  2. 指令操作數.
  3. 指令順序.

為理解Instruction如何描述這些信息, 我們先來看下其定義, Instruction繼承自兩個類User與ilist, 其中鏈表ilist用於串聯指令流使其可以被順序訪問.
為支持ilist的組織結構, Instruction包含一個Parent成員保存其所屬的基礎塊指針以及一個用於計算dominance的Order. 另外Instruction還包含了一個DbgLoc用於記錄Debug信息.

class Instruction : public User,
                    public ilist_node_with_parent<Instruction, BasicBlock> {
  BasicBlock *Parent;
  DebugLoc DbgLoc;

  mutable unsigned Order = 0;

protected:
  ~Instruction();

public:
  Instruction(const Instruction &) = delete;
  Instruction &operator=(const Instruction &) = delete;

  unsigned getOpcode() const { return getValueID() - InstructionVal; }
  const char *getOpcodeName() const { return getOpcodeName(getOpcode()); }
  static const char* getOpcodeName(unsigned OpCode);

  inline const BasicBlock *getParent() const { return Parent; }
  inline       BasicBlock *getParent()       { return Parent; }
};

指令類型

Instruction::getOpcode()用於返回指令類型, 其值由Value::getValueID()減去Value::InstructionVal得到.
后者在上節提到過, 即Value::SubClassID中大於Value::InstructionVal的枚舉值屬於指令類型枚舉, 它們定義在include/llvm/IR/Instruction.def中.

  enum UnaryOps {
#define  FIRST_UNARY_INST(N)             UnaryOpsBegin = N,
#define HANDLE_UNARY_INST(N, OPC, CLASS) OPC = N,
#define   LAST_UNARY_INST(N)             UnaryOpsEnd = N+1
#include "llvm/IR/Instruction.def"
  };

  enum BinaryOps {
#define  FIRST_BINARY_INST(N)             BinaryOpsBegin = N,
#define HANDLE_BINARY_INST(N, OPC, CLASS) OPC = N,
#define   LAST_BINARY_INST(N)             BinaryOpsEnd = N+1
#include "llvm/IR/Instruction.def"
  };

  enum MemoryOps {
#define  FIRST_MEMORY_INST(N)             MemoryOpsBegin = N,
#define HANDLE_MEMORY_INST(N, OPC, CLASS) OPC = N,
#define   LAST_MEMORY_INST(N)             MemoryOpsEnd = N+1
#include "llvm/IR/Instruction.def"
  };

以上指令的類型對應IR輸出可見Instruction::getOpcodeName()(defined in lib/IR/Instruction.cpp).

指令操作數

User類已經定義了一系列訪問其依賴的值(操作數), 因此Instruction只需繼承User即可, 回顧一下User類接口.
User::getOperand()User::setOperand()用來獲取/修改給定下標的指令的操作數, 如果需要迭代遍歷操作數則可以使用User::operands().

class User : public Value {
public:
  Value *getOperand(unsigned i) const {
    assert(i < NumUserOperands && "getOperand() out of range!");
    return getOperandList()[i];
  }

  void setOperand(unsigned i, Value *Val) {
    assert(i < NumUserOperands && "setOperand() out of range!");
    assert((!isa<Constant>((const Value*)this) ||
            isa<GlobalValue>((const Value*)this)) &&
           "Cannot mutate a constant with setOperand!");
    getOperandList()[i] = Val;
  }

  using op_iterator = Use*;
  using const_op_iterator = const Use*;
  using op_range = iterator_range<op_iterator>;
  using const_op_range = iterator_range<const_op_iterator>;

  op_iterator       op_begin()       { return getOperandList(); }
  const_op_iterator op_begin() const { return getOperandList(); }
  op_iterator       op_end()         {
    return getOperandList() + NumUserOperands;
  }
  const_op_iterator op_end()   const {
    return getOperandList() + NumUserOperands;
  }
  op_range operands() {
    return op_range(op_begin(), op_end());
  }
  const_op_range operands() const {
    return const_op_range(op_begin(), op_end());
  }

  ......
};

指令順序

一般情況下, 一個函數包含若干個基礎塊, 而一個基礎塊又包含若干條(線性排布的)指令, 因此需要一個恰當的數據結構來管理指令流. 注意到:

  1. 在編譯器開發中常常需要在非固定位置插入/刪除指令, 意味着單純的順序容器不能滿足高效修改的需求.
  2. 由於底層數據結構(Value)中有向圖的設計, 編譯器可以高效的查找/更新指令位置, 對隨機訪問的需求也不大.

基於這個背景, 作為有向圖的補充, LLVM使用鏈表來管理指令流. Instruction使用名為ilist_node_with_parent的鏈表來鏈接指令. 這個鏈表有以下幾個特點:

  1. 侵入式設計, 即鏈表節點作為基類被其管理的Instruction類繼承, 作為其繼承類的一部分, 鏈表節點的構造也在構造繼承類對象時構造.
  2. 相比於普通鏈表還包含一個父節點(parent), 指示其所屬的上層對象類型. 繼承類需要實現getParent()方法獲取其所屬的上層對象的指針. 上層對象需要實現getSublistAccess()方法獲取鏈表.
  3. 部分包含所有權語義, 即刪除鏈表節點時會同時刪除繼承類對象(然而構造鏈表節點時不會).

讓我們來看下具體實現, 首先如上文所見Instruction包含一個Parent指針, 指向其所屬的基礎塊. Instruction::getParent()用來返回該指針.
再來看下上層對象BasicBlock怎么返回Instruction鏈表. 其包含一個SymbolTableList 成員.
方法 BasicBlock::*getSublistAccess()返回了該鏈表成員, 注意這個接口是給鏈表內部邏輯代碼使用的, 通常我們使用 BasicBlock::getInstList()獲取鏈表.

class BasicBlock final : public Value,
                         public ilist_node_with_parent<BasicBlock, Function> {
public:
  using InstListType = SymbolTableList<Instruction>;

private:
  InstListType InstList;

public:
  using iterator = InstListType::iterator;
  using const_iterator = InstListType::const_iterator;
  using reverse_iterator = InstListType::reverse_iterator;
  using const_reverse_iterator = InstListType::const_reverse_iterator;

  const InstListType &getInstList() const { return InstList; }
        InstListType &getInstList()       { return InstList; }

  /// Returns a pointer to a member of the instruction list.
  static InstListType BasicBlock::*getSublistAccess(Instruction*) {
    return &BasicBlock::InstList;
  }

  ......
};

SymbolTableList(include/llvm/IR/SymbolTableListTraits.h)是iplist_impl的偏特化, 關於iplist的詳細介紹可以看這里, 這里我們看下其特化的萃取器SymbolTableListTraits.

template <class T> class SymbolTableList : public iplist_impl<simple_ilist<T>, SymbolTableListTraits<T>> {};

SymbolTableListTraits是一類特殊的traits, 其設計目的是在子節點鏈表發生變化時通知父節點以及符號表更新, 幫助編譯器開發者從繁瑣的工作中解脫出來.

template <typename NodeTy> struct SymbolTableListParentType {};

#define DEFINE_SYMBOL_TABLE_PARENT_TYPE(NODE, PARENT)                          \
  template <> struct SymbolTableListParentType<NODE> { using type = PARENT; };
DEFINE_SYMBOL_TABLE_PARENT_TYPE(Instruction, BasicBlock)
DEFINE_SYMBOL_TABLE_PARENT_TYPE(BasicBlock, Function)
DEFINE_SYMBOL_TABLE_PARENT_TYPE(Argument, Function)
DEFINE_SYMBOL_TABLE_PARENT_TYPE(Function, Module)
DEFINE_SYMBOL_TABLE_PARENT_TYPE(GlobalVariable, Module)
DEFINE_SYMBOL_TABLE_PARENT_TYPE(GlobalAlias, Module)
DEFINE_SYMBOL_TABLE_PARENT_TYPE(GlobalIFunc, Module)
#undef DEFINE_SYMBOL_TABLE_PARENT_TYPE

class SymbolTableListTraits : public ilist_alloc_traits<ValueSubClass> {
  using ListTy = SymbolTableList<ValueSubClass>;
  using iterator = typename simple_ilist<ValueSubClass>::iterator;
  using ItemParentClass = typename SymbolTableListParentType<ValueSubClass>::type;

public:
  SymbolTableListTraits() = default;

private:
  ItemParentClass *getListOwner() {
    size_t Offset(size_t(&((ItemParentClass*)nullptr->*ItemParentClass::
                           getSublistAccess(static_cast<ValueSubClass*>(nullptr)))));
    ListTy *Anchor(static_cast<ListTy *>(this));
    return reinterpret_cast<ItemParentClass*>(reinterpret_cast<char*>(Anchor) - Offset);
  }

  static ListTy &getList(ItemParentClass *Par) {
    return Par->*(Par->getSublistAccess((ValueSubClass*)nullptr));
  }

  static ValueSymbolTable *getSymTab(ItemParentClass *Par) {
    return Par ? toPtr(Par->getValueSymbolTable()) : nullptr;
  }

public:
  void addNodeToList(ValueSubClass *V);
  void removeNodeFromList(ValueSubClass *V);
  void transferNodesFromList(SymbolTableListTraits &L2, iterator first, iterator last);
  // private:
  template<typename TPtr> void setSymTabObject(TPtr *, TPtr);
  static ValueSymbolTable *toPtr(ValueSymbolTable *P) { return P; }
  static ValueSymbolTable *toPtr(ValueSymbolTable &R) { return &R; }
};

template <typename ValueSubClass>
void SymbolTableListTraits<ValueSubClass>::addNodeToList(ValueSubClass *V) {
  assert(!V->getParent() && "Value already in a container!!");
  ItemParentClass *Owner = getListOwner();
  V->setParent(Owner);
  invalidateParentIListOrdering(Owner);
  if (V->hasName())
    if (ValueSymbolTable *ST = getSymTab(Owner))
      ST->reinsertValue(V);
}

template <typename ValueSubClass>
void SymbolTableListTraits<ValueSubClass>::removeNodeFromList(ValueSubClass *V) {
  V->setParent(nullptr);
  if (V->hasName())
    if (ValueSymbolTable *ST = getSymTab(getListOwner()))
      ST->removeValueName(V->getValueName());
}

SymbolTableListTraits同樣繼承自ilist_alloc_traits. 注意這里ValueSubClass是子節點(Instruction), 而ItemParentClass是父節點(BasicBlock), 其定義見宏DEFINE_SYMBOL_TABLE_PARENT_TYPE特化了一系列SymbolTableListParentType模板.
SymbolTableListTraits是如何自動更新鏈表的關系的呢? 可以通過addNodeToList()簡單窺探一下. 通過getListOwner()獲取鏈表父節點, 然后更新節點的Parent為對應值, 再調用invalidateParentIListOrdering()將父節點Order設為非法值.

擴展指令

基於Instruction類擴展出了LLVM IR中的指令, 大致上它們分為若干類.

class name concrete instruction
UnaryInstruction 只包含單一操作數的指令(使用HANDLE_UNARY_INST宏的指令), 另外Alloca/Load/VAArg/Cast也屬於UnaryInstruction.
BinaryOperator 包含二元操作數的指令(HANDLE_BINARY_INST).
CallBase Call/Invoke/Callbr三類指令.
StoreInst Store指令.
GetElementPtrInst GetElementPtr指令.
SelectInst Select指令.
PHINode PHI指令.

還有少量指令(CAS指令, 原子讀寫指令, 矢量shuffle指令)的分類就不一一列舉了. 總體而言, LLVM IR按照指令操作數進行分類, 對於一些具有高層語言抽象的指令(phi指令, gep指令)則單獨分類.

修改IR

從上文介紹可以發現修改LLVM IR的指令流並不容易. 首先需要新建對象, 正確設置每個操作數, 其次需要將其加入合適的鏈表, 更新對應父節點的數據結構與符號表.
所幸LLVM提供了名為IRBuilder(defined in include/llvm/IR/IRBuilder.h)的適配器類減輕了我們的開發工作. 以PHI指令為例:

static PHINode *PHINode::Create(Type *Ty, unsigned NumReservedValues,
                                const Twine &NameStr = "",
                                Instruction *InsertBefore = nullptr) {
  return new PHINode(Ty, NumReservedValues, NameStr, InsertBefore);
}

PHINode *IRBuilderBase::CreatePHI(Type *Ty, unsigned NumReservedValues, const Twine &Name = "") {
  PHINode *Phi = PHINode::Create(Ty, NumReservedValues);
  if (isa<FPMathOperator>(Phi))
    setFPAttrs(Phi, nullptr /* MDNode* */, FMF);
  return Insert(Phi, Name);
}

template<typename InstTy>
InstTy *IRBuilderBase::Insert(InstTy *I, const Twine &Name = "") const {
  Inserter.InsertHelper(I, Name, BB, InsertPt);
  SetInstDebugLocation(I);
  return I;
}

template <typename FolderTy = ConstantFolder,
          typename InserterTy = IRBuilderDefaultInserter>
class IRBuilder : public IRBuilderBase {
private:
  FolderTy Folder;
  InserterTy Inserter;

  ......
};

IRBuilder::CreatePHI()調用PHINode類的靜態方法Create()創建一個PHINode對象, 然后調用IRBuilder::Insert()將其插入至基礎塊中.
其插入方式由IRBuilder的模板參數InsertTy指定, 默認使用IRBuilderDefaultInserter, 即在制定位置InsertPt插入指令.


免責聲明!

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



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