上節介紹了IR中底層的數據表達方式(Value)及其組織方式(有向圖), 有了這個基礎就可以理解LLVM IR中的一些基本構成. 本節將要介紹其中的一個基礎概念: 指令(Instruction).
LLVM IR指令基礎
LLVM使用Instruction來描述一條具體的指令. 與ISA設計類似, 在LLVM中指令可以細分為若干類:
- 算術與位運算指令: 絕大部分指令可以歸屬為一元操作數指令(UnaryInstruction)與二元操作數指令(BinaryInstruction).
- 控制流指令: 包含(函數內)跳轉指令(branch/indirect branch), 函數調用與返回(call/invoke/ret), 分支跳轉(switch)等改變控制流的指令.
- 訪存指令: 操作內存的指令(load/store).
- 其它指令: 不同於常見ISA的特有指令, 比如比較指令(icmp), 選擇賦值指令(select), 以及維護SSA的必需品phi指令.
關於更多的IR指令介紹可以參見官方文檔. 本文假定讀者已經對IR有了基礎了解, 將更多注重代碼實現細節.
所有的指令都從Instruction類繼承而來, 不同分類的指令有不同的特性. 我們首先看下其基礎結構與實現邏輯, 再針對性介紹幾類指令的細節.
通用指令定義
LLVM使用Instruction(defined in include/llvm/IR/Instruction.h)類來描述一條通用指令, 這里的通用的含義是所有指令都具有的行為(比如操作數信息), 具體而言包括:
- 指令類型.
- 指令操作數.
- 指令順序.
為理解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());
}
......
};
指令順序
一般情況下, 一個函數包含若干個基礎塊, 而一個基礎塊又包含若干條(線性排布的)指令, 因此需要一個恰當的數據結構來管理指令流. 注意到:
- 在編譯器開發中常常需要在非固定位置插入/刪除指令, 意味着單純的順序容器不能滿足高效修改的需求.
- 由於底層數據結構(Value)中有向圖的設計, 編譯器可以高效的查找/更新指令位置, 對隨機訪問的需求也不大.
基於這個背景, 作為有向圖的補充, LLVM使用鏈表來管理指令流. Instruction使用名為ilist_node_with_parent的鏈表來鏈接指令. 這個鏈表有以下幾個特點:
- 侵入式設計, 即鏈表節點作為基類被其管理的Instruction類繼承, 作為其繼承類的一部分, 鏈表節點的構造也在構造繼承類對象時構造.
- 相比於普通鏈表還包含一個父節點(parent), 指示其所屬的上層對象類型. 繼承類需要實現
getParent()方法獲取其所屬的上層對象的指針. 上層對象需要實現getSublistAccess()方法獲取鏈表. - 部分包含所有權語義, 即刪除鏈表節點時會同時刪除繼承類對象(然而構造鏈表節點時不會).
讓我們來看下具體實現, 首先如上文所見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插入指令.
