自從年初手斷了以后好久不寫文字了, 說好的筆耕不輟也忘了(=.=), 今天正好有同學問AntiDepBreaker, 就花點時間看了下代碼(順便水一篇).
背景概述
1. 什么是Anti Dependency
在數據流分析中一般將數據依賴分為三種:
- 寫后讀(RAW, read after write), 通常情況下一個值必須先定義后使用, 因此又稱數據依賴(data dependency)或真依賴(true dependency).
- 讀后寫(WAR, write after read), 這種情況通常發生在對同一變量/寄存器連續讀寫之間, 由於讀的是前一次寫的值, 而寫的是后一次的值, 因此被稱為反依賴(anti dependency).
- 寫后寫(WAW, write after write), 對同一變量/寄存器連續寫, 被稱為輸出依賴(output dependency).
在LLVM調度器中使用SDep(schedule dependency)描述兩個SUnit(schedule unit)之間的依賴(見include/llvm/CodeGen/ScheduleDAG.h).
2. 什么情況下出現反依賴
對於反依賴與輸出依賴, 由於它們分別要求先使用后定義/多次定義, 因此只有在經過Phi Elimination后程序流不再維護SSA狀態才能出現. 而一般架構在postRA階段會再做一次指令調度, 此時分析依賴圖即會產生反依賴與輸出依賴.
另外需要注意的是有些架構存在隱式定義/使用特殊寄存器的情況, 由於這類指令固定使用某些物理寄存器, 因此preRA階段也可能產生針對特定物理寄存器的反依賴/數據依賴(這也是為什么建議大家不要給指令定義隱式急寄存器, 而是通過增加寄存器類型的方式限定寄存器使用的原因).
3. AntiDepBreaker的作用
依賴的存在加強了節點間的約束, 使指令調度時可選的調度窗口變窄, 導致調度結果變差. 減少依賴可以擴大指令調度窗口, 縮短critical path的長度, 獲得更好的性能.
需要注意的是真依賴與(對固定寄存器的)寫依賴/反依賴都是不可消除的, 只有(對非固定寄存器的)寫依賴/反依賴其讀寫的內容實質指向兩個值, 因此可以通過變量/寄存器重命名來解決, AntiDepBreaker就是用postRA階段調度前的消除依賴的優化.
另外還要注意的是這類優化主要針對沒有硬件調度單元的架構做的, 對於支持亂序執行與重命名的架構作用相對而言較小.
4. AntiDepBreaker的實現思路
自底向上掃描指令, 跟蹤寄存器分配狀態的同時檢查指令是否存在反依賴. 若存在反依賴則從空閑的寄存器列表中選擇合適(生命周期不沖突)的寄存器做替換.
在postRA做這類數據流分析的難度主要在於無法維持SSA狀態, 在分析過程中需要注意以下幾點:
- 分析別名寄存器. 在LLVM中物理寄存器不同與虛擬寄存器, 不同類型的虛擬寄存器在preRA階段存在COPY/PHI/REG_SEQUENCE等方式轉換寄存器類型, 但postRA階段對應的偽指令被消除, 判斷寄存器分類時需要考慮子集/超集.
- 各式各樣的寄存器標記. 類似1的原因, 在postRA階段寄存器標記遠遠多於preRA階段, 不同的寄存器標簽影響寄存器生命周期的計算.
- 候選寄存器的判斷. 原則上只要生命周期不沖突的寄存器都可以做候選的替換寄存器, 但是考慮函數調用, live in/out等情況, 實際可替換的候選少於理想情況.
實現分析
AntiDepBreaker不是一個單獨的PASS, 而是依附於postRA階段的調度PASS, 其原因是postRA調度按region划分並構建依賴圖, 因此每個region調度前都需要調用AntiDepBreaker. 可以通過選項-mllvm -break-anti-dependencies=none
關閉該優化(見lib/CodeGen/PostRASchedulerList.cpp:61).
在開啟該優化時也可以選擇兩種模式: critical與aggressive, 其中前者只會針對關鍵路徑上的反依賴做消除, 后者則會力求消除所有的反依賴, 可以通過上面的選項控制使用哪種模式.
我們以aggressive模式為例來看看如何實現AntiDepBreaker(源碼見lib/CodeGen/AggressiveAntiDepBreaker.cpp).
先來看下數據結構的定義: AggressiveAntiDepBreaker定義了兩個類(見lib/CodeGen/AggressiveAntiDepBreaker.h), AggressiveAntiDepState與AggressiveAntiDepBreaker.
AggressiveAntiDepState
AggressiveAntiDepState用於在遍歷指令的過程中維護寄存器的狀態, 其成員如下.
class LLVM_LIBRARY_VISIBILITY AggressiveAntiDepState {
const unsigned NumTargetRegs;
std::vector<unsigned> GroupNodes;
std::vector<unsigned> GroupNodeIndices;
std::multimap<unsigned, RegisterReference> RegRefs;
std::vector<unsigned> KillIndices;
std::vector<unsigned> DefIndices;
};
AggressiveAntiDepState::AggressiveAntiDepState(const unsigned TargetRegs,
MachineBasicBlock *BB)
: NumTargetRegs(TargetRegs), GroupNodes(TargetRegs, 0),
GroupNodeIndices(TargetRegs, 0), KillIndices(TargetRegs, 0),
DefIndices(TargetRegs, 0) {
const unsigned BBSize = BB->size();
for (unsigned i = 0; i < NumTargetRegs; ++i) {
GroupNodeIndices[i] = i;
KillIndices[i] = ~0u;
DefIndices[i] = BBSize;
}
}
- NumTargetRegs是一個常量值, 該值為架構的寄存器總數(
TRI->getNumRegs()
), 為其它幾個容器成員的初始元素個數. - 容器KillIndices與DefIndices分別用來記錄寄存器的kill/define狀態, 其下標為寄存器序號, 其值為kill/define該寄存器的指令(在本BB內的)序號. 需要注意的是值~0為特殊值, 分別表示該寄存器未存活/寄存器存活.
- GroupNodes與GroupNodeIndices實現了一個並查集, 用來為具有別名的寄存器分類. 其中后者記錄了每個寄存器在分組(前者)中的序號, 前者則保存了同屬一個分組的前一元素的序號(當指向自己時為根節點). 在分析的過程中發現某一寄存器存活則將所有別名寄存器都標記為該分組.
- RegRefs記錄了寄存器(在一段生命周期內)的所有使用者.
補充: 寄存器的幾種狀態(關於寄存器狀態定義的細節見include/llvm/CodeGen/MachineOperand.h中定義).
- define: 若一條指令寫一個寄存器則稱指令定義(define)了該寄存器.
- kill: 一個寄存器存在零到若干個使用者, 其中最后一個使用者使用后該寄存器作用消失, 因此稱最后一個使用者殺死(kill)了該寄存器.
- dead: 若一個寄存器沒有使用者, 則該寄存器為冗余的, 稱該定義的寄存器(在被定義的同時)是死亡(dead)的.
- partial define: 若一個寄存器由若干寄存器組成(通常包含兩種情況, 一是像X86架構eax/rax這種A寄存器在物理上是B寄存器的一部分, 二是軟件上有時將連續的寄存器組合為一個超集), 且其中一部分被定義則稱該寄存器為partial define. 其中前者情況在preRA前通常有寄存器A到寄存器B的偽拷貝指令, 后者通常會由一個REQ_SUQUENCE偽指令組合而成, 但是兩者在寄存器分配后均被消除, 需要遍歷別名寄存器來判斷是否是partial define.
- tied: 若一條指令在讀一個寄存器的同時又寫了它則稱這個使用者與定義者是綁定(tied)的.
再看下AggressiveAntiDepState對外暴露的幾個接口.
unsigned AggressiveAntiDepState::GetGroup(unsigned Reg) {
unsigned Node = GroupNodeIndices[Reg];
while (GroupNodes[Node] != Node)
Node = GroupNodes[Node];
return Node;
}
void AggressiveAntiDepState::GetGroupRegs(
unsigned Group,
std::vector<unsigned> &Regs,
std::multimap<unsigned, AggressiveAntiDepState::RegisterReference> *RegRefs)
{
for (unsigned Reg = 0; Reg != NumTargetRegs; ++Reg) {
if ((GetGroup(Reg) == Group) && (RegRefs->count(Reg) > 0))
Regs.push_back(Reg);
}
}
unsigned AggressiveAntiDepState::UnionGroups(unsigned Reg1, unsigned Reg2) {
unsigned Group1 = GetGroup(Reg1);
unsigned Group2 = GetGroup(Reg2);
unsigned Parent = (Group1 == 0) ? Group1 : Group2;
unsigned Other = (Parent == Group1) ? Group2 : Group1;
GroupNodes.at(Other) = Parent;
return Parent;
}
unsigned AggressiveAntiDepState::LeaveGroup(unsigned Reg) {
unsigned idx = GroupNodes.size();
GroupNodes.push_back(idx);
GroupNodeIndices[Reg] = idx;
return idx;
}
bool AggressiveAntiDepState::IsLive(unsigned Reg) {
return((KillIndices[Reg] != ~0u) && (DefIndices[Reg] == ~0u));
}
AggressiveAntiDepState主要提供了操作並查集的接口, 其中GetGroup()返回給定寄存器所在的分組, UnionGroups()將兩個分組合並, LeaveGroup()將給定寄存器提出分組. 需要注意的是LeaveGroup()在將寄存器踢出分組時並不會修改原有的分組, 而是新建一個分組, 這是因為當前節點可能被別的節點所引用(即在一條鏈路的中間).
AggressiveAntiDepBreaker
再來看下另一個類AggressiveAntiDepBreaker, 其主要成員是一個指向AggressiveAntiDepState的指針(State)以及一個BitVector(CriticalPathSet), 其基類AntiDepBreaker封裝了對外的接口.
class LLVM_LIBRARY_VISIBILITY AggressiveAntiDepBreaker
: public AntiDepBreaker {
MachineFunction &MF;
MachineRegisterInfo &MRI;
const TargetInstrInfo *TII;
const TargetRegisterInfo *TRI;
const RegisterClassInfo &RegClassInfo;
BitVector CriticalPathSet;
AggressiveAntiDepState *State = nullptr;
public:
void StartBlock(MachineBasicBlock *BB) override;
unsigned BreakAntiDependencies(const std::vector<SUnit> &SUnits,
MachineBasicBlock::iterator Begin,
MachineBasicBlock::iterator End,
unsigned InsertPosIndex,
DbgValueVector &DbgValues) override;
void Observe(MachineInstr &MI, unsigned Count,
unsigned InsertPosIndex) override;
void FinishBlock() override;
};
class LLVM_LIBRARY_VISIBILITY AntiDepBreaker {
public:
virtual void StartBlock(MachineBasicBlock *BB) = 0;
virtual unsigned BreakAntiDependencies(const std::vector<SUnit> &SUnits,
MachineBasicBlock::iterator Begin,
MachineBasicBlock::iterator End,
unsigned InsertPosIndex,
DbgValueVector &DbgValues) = 0;
virtual void Observe(MachineInstr &MI, unsigned Count,
unsigned InsertPosIndex) = 0;
virtual void FinishBlock() = 0;
};
AntiDepBreaker包含四個接口, 分別作為調度器代碼的四個流程(starBlock/schedule/Observe/finishBlock)的hook, 下面逐一分析.
StartBlock()
在每個Block被調度前首先需要執行StartBlock()做准備工作, 其主要內容為初始化寄存器信息的初始狀態.
void AggressiveAntiDepBreaker::StartBlock(MachineBasicBlock *BB) {
State = new AggressiveAntiDepState(TRI->getNumRegs(), BB);
bool IsReturnBlock = BB->isReturnBlock();
std::vector<unsigned> &KillIndices = State->GetKillIndices();
std::vector<unsigned> &DefIndices = State->GetDefIndices();
for (MachineBasicBlock::succ_iterator SI = BB->succ_begin(),
SE = BB->succ_end(); SI != SE; ++SI)
for (const auto &LI : (*SI)->liveins()) {
for (MCRegAliasIterator AI(LI.PhysReg, TRI, true); AI.isValid(); ++AI) {
unsigned Reg = *AI;
State->UnionGroups(Reg, 0);
KillIndices[Reg] = BB->size();
DefIndices[Reg] = ~0u;
}
}
const MachineFrameInfo &MFI = MF.getFrameInfo();
BitVector Pristine = MFI.getPristineRegs(MF);
for (const MCPhysReg *I = MF.getRegInfo().getCalleeSavedRegs(); *I;
++I) {
unsigned Reg = *I;
if (!IsReturnBlock && !Pristine.test(Reg))
continue;
for (MCRegAliasIterator AI(Reg, TRI, true); AI.isValid(); ++AI) {
unsigned AliasReg = *AI;
State->UnionGroups(AliasReg, 0);
KillIndices[AliasReg] = BB->size();
DefIndices[AliasReg] = ~0u;
}
}
}
函數首先構造並初始化一個AggressiveAntiDepState, 由於調度過程是自底向上(bottom-up)的, 因此初始化時需要考慮本Block的live out. 通常情況下本Block的live out即本Block的后繼的live in的集合, 但是在這里還需加上live out的callee saved寄存器. 這是因為記錄KillIndices與DefIndices的目的是判斷寄存器是否可用, 而live out的callee saved寄存器雖然沒有生命周期(沒有定義與使用), 但同時也不是可用的寄存器. 這類寄存器包含兩塊: 對return block而言即所有callee saved寄存器, 對於非return block而言則是未在prolog里保存的callee save的寄存器.
BreakAntiDependencies()
BreakAntiDependencies()是處理反依賴消除的核心代碼, 源代碼較多這里僅列出幾個重要步驟.
unsigned AggressiveAntiDepBreaker::BreakAntiDependencies(
const std::vector<SUnit> &SUnits,
MachineBasicBlock::iterator Begin,
MachineBasicBlock::iterator End,
unsigned InsertPosIndex,
DbgValueVector &DbgValues) {
......
for (MachineBasicBlock::iterator I = End, E = Begin;
I != E; --Count) {
MachineInstr &MI = *--I;
std::set<unsigned> PassthruRegs;
GetPassthruRegs(MI, PassthruRegs);
PrescanInstruction(MI, Count, PassthruRegs);
std::vector<const SDep *> Edges;
const SUnit *PathSU = MISUnitMap[&MI];
AntiDepEdges(PathSU, Edges);
for (unsigned i = 0, e = Edges.size(); i != e; ++i) {
const SDep *Edge = Edges[i];
SUnit *NextSU = Edge->getSUnit();
if ((Edge->getKind() != SDep::Anti) &&
(Edge->getKind() != SDep::Output)) continue;
......
std::map<unsigned, unsigned> RenameMap;
if (FindSuitableFreeRegisters(GroupIndex, RenameOrder, RenameMap)) {
for (std::map<unsigned, unsigned>::iterator
S = RenameMap.begin(), E = RenameMap.end(); S != E; ++S) {
State->UnionGroups(NewReg, 0);
RegRefs.erase(NewReg);
DefIndices[NewReg] = DefIndices[CurrReg];
KillIndices[NewReg] = KillIndices[CurrReg];
State->UnionGroups(CurrReg, 0);
RegRefs.erase(CurrReg);
DefIndices[CurrReg] = KillIndices[CurrReg];
KillIndices[CurrReg] = ~0u;
}
}
}
ScanInstruction(MI, Count);
}
}
bool AggressiveAntiDepBreaker::FindSuitableFreeRegisters(
unsigned AntiDepGroupIndex,
RenameOrderType& RenameOrder,
std::map<unsigned, unsigned> &RenameMap) {
std::vector<unsigned> Regs;
State->GetGroupRegs(AntiDepGroupIndex, Regs, &RegRefs);
for (unsigned i = 0, e = Regs.size(); i != e; ++i) {
unsigned Reg = Regs[i];
if ((SuperReg == 0) || TRI->isSuperRegister(SuperReg, Reg))
SuperReg = Reg;
if (RegRefs.count(Reg) > 0) {
BitVector &BV = RenameRegisterMap[Reg];
BV = GetRenameRegisters(Reg);
}
}
const TargetRegisterClass *SuperRC =
TRI->getMinimalPhysRegClass(SuperReg, MVT::Other);
RenameOrder.insert(RenameOrderType::value_type(SuperRC, Order.size()));
unsigned OrigR = RenameOrder[SuperRC];
unsigned EndR = ((OrigR == Order.size()) ? 0 : OrigR);
unsigned R = OrigR;
do {
for (unsigned i = 0, e = Regs.size(); i != e; ++i) {
if (State->IsLive(NewReg) || (KillIndices[Reg] > DefIndices[NewReg])) {
goto next_super_reg;
} else {
bool found = false;
for (MCRegAliasIterator AI(NewReg, TRI, false); AI.isValid(); ++AI) {
unsigned AliasReg = *AI;
if (State->IsLive(AliasReg) ||
(KillIndices[Reg] > DefIndices[AliasReg])) {
LLVM_DEBUG(dbgs()
<< "(alias " << printReg(AliasReg, TRI) << " live)");
found = true;
break;
}
}
if (found)
goto next_super_reg;
}
......
}
}
}
處理邏輯是從給定的迭代器開始逆序遍歷指令, 對於每條指令定義的寄存器分析其定義的寄存器並更新寄存器狀態(AggressiveAntiDepState). 然后在判斷指令是否存在依賴, 判斷打破依賴可能性. 如果確認需要打破依賴則調用FindSuitableFreeRegisters()獲取可替換的寄存器列表, 嘗試替換寄存器. 最后(對於重命名的情況)調用ScanInstruction()更新寄存器狀態. 其中:
- GetPassthruRegs()用來處理單條指令同時包含(對同一寄存器的)定義與使用的特殊情況(即寄存器生命周期pass through這條指令), 對於這類情況寄存器生命周期默認延長(至前一個定義). 什么情況下會產生pass through? 注釋已經解釋了: 指令包含tied寄存器或隱式使用特殊寄存器的情況.
- PrescanInstruction()用來處理指令定義的寄存器, 並負責更新其狀態. 對於每個寄存器定義如果該寄存器當前存活則將所有別名寄存器加入相同分組並更新DefIndices. 這里需要注意的幾種特殊情況的處理, 一是dead定義寄存器會先為它生成一個偽的使用者(這步在HandleLastUse()中處理). 原因是dead定義沒有使用者, 因此不會更新分組, 而是被延遲到前一定義處理, 導致生命周期變相延長. 二是針對函數調用(或內聯匯編或其它特殊寄存器分配需求)的情況, 為維護ABI, 簡化處理, 會將定義的寄存器設置為不可替換的寄存器(分組0).
- AntiDepEdges()查詢SUnit並返回指令上的反依賴與寫依賴. 不是所有的依賴都會做消除, 以下若干種情況不會處理: 如果寄存器是預留寄存器或所屬的寄存器類不可分配(MRI.isAllocatable()), 寄存器屬於關鍵路徑寄存器但指令又不在關鍵路徑上, 寄存器是隱式寄存器或pass through寄存器或寄存器存在多個反依賴.
- 查找可選寄存器的實現見FindSuitableFreeRegisters(), 其思路是首先通過先前保存的並查集找到被替換寄存器所在的分組, 該分組中可能包含被替換寄存器的超集, 為滿足所有寄存器的分配需求需要找到最大的超集寄存器類. 在找到該類后遍歷所屬該類的寄存器, 查看它們在當前節點是否存活即define與kill狀態. 若存活或其定義(DefIndices)小於被替換寄存器的最后的使用(KillIndices)則說明兩者生命周期沖突, 不可替換. 否則進行下一步檢測: 遍歷該寄存器的別名寄存器分析其生命周期是否與被替換寄存器沖突.
- 若成功找到一個候選則執行替換, 替換過程會將所有被替換寄存器的別名寄存器都執行替換: 更新寄存器分組, KillIndices與DefIndices.
- 最后執行ScanInstruction()更新寄存器的live range. 其實現與PrescanInstruction()類似, 但是處理的是use(define在前一步已完成處理).
Observe()與FinishBlock()
比較簡單不再贅述.
小結
相比於preRA, (LLVM中)postRA的數據流分析優化較少, 一方面原因是postRA不再維護SSA狀態另一方面考慮到復雜的寄存器別名分析. AntiDepBreaker給我們很好的展示了在postRA做變換時需要考慮的若干關鍵點. 另一方面從代碼中也可以看到其分析較為保守(候選寄存器的限定條件), 而其分析處理的情況較多, 因此有時優化結果不理想也是可以預見的.