LLVM 程式設計師手冊¶
警告
這始終是一項進行中的工作。
簡介¶
本文檔旨在重點介紹 LLVM 原始碼庫中提供的一些重要類別和介面。本手冊不旨在解釋 LLVM 是什麼、它的運作方式以及 LLVM 程式碼的外觀。它假設您了解 LLVM 的基礎知識,並且有興趣編寫轉換或以其他方式分析或操作程式碼。
本文檔應引導您入門,以便您可以在不斷增長的構成 LLVM 基礎架構的原始碼中找到方向。請注意,本手冊並非旨在取代閱讀原始碼,因此如果您認為這些類別中應該有一個方法來執行某些操作,但未列出,請檢查原始碼。提供了 doxygen 來源的連結,以便盡可能輕鬆地完成此操作。
本文檔的第一節描述了一般資訊,這些資訊在 LLVM 基礎架構中工作時很有用,第二節描述了核心 LLVM 類別。未來,本手冊將擴展資訊,描述如何使用擴展函式庫,例如支配者資訊、CFG 遍歷常式以及有用的工具,如 InstVisitor
( doxygen ) 範本。
一般資訊¶
本節包含一般資訊,如果您在 LLVM 原始碼庫中工作,這些資訊很有用,但這些資訊並非特定於任何特定的 API。
C++ 標準範本庫¶
LLVM 大量使用 C++ 標準範本庫 (STL),可能比您習慣或以前見過的使用量還要多。因此,您可能需要稍微背景閱讀一下該庫中使用的技術和功能。有許多好的頁面討論 STL,以及一些關於該主題的書籍可以取得,因此本文檔將不討論它。
以下是一些有用的連結
cppreference.com - STL 和標準 C++ 庫其他部分的優秀參考資料。
cplusplus.com - 另一個像上面一樣優秀的參考資料。
C++ In a Nutshell - 這是一本正在製作中的 O’Reilly 書籍。它有一個不錯的標準庫參考,可以與 Dinkumware 的相媲美,但不幸的是,自本書出版以來,它不再免費。
也鼓勵您查看 LLVM 編碼標準 指南,該指南側重於如何編寫可維護的程式碼,而不是將大括號放在哪裡。
其他有用的參考資料¶
重要且實用的 LLVM API¶
在這裡,我們重點介紹一些 LLVM API,這些 API 通常很有用,並且在編寫轉換時最好了解它們。
isa<>
、cast<>
和 dyn_cast<>
範本¶
LLVM 原始碼庫廣泛使用自訂形式的 RTTI。這些範本與 C++ dynamic_cast<>
運算子有許多相似之處,但它們沒有一些缺點 (主要源於 dynamic_cast<>
僅適用於具有 v 表的類別這一事實)。由於它們經常被使用,您必須知道它們的作用以及它們的工作方式。所有這些範本都在 llvm/Support/Casting.h
( doxygen ) 檔案中定義 (請注意,您很少需要直接包含此檔案)。
isa<>
:isa<>
運算子的工作方式與 Java “instanceof
” 運算子完全相同。它根據參考或指標是否指向指定類別的實例傳回 true 或 false。這對於各種約束檢查非常有用 (範例如下)。cast<>
:cast<>
運算子是“已檢查的轉換”操作。它將指標或參考從基底類別轉換為衍生類別,如果它實際上不是正確類型的實例,則會導致斷言失敗。這應該用於您有一些資訊讓您相信某事物是正確類型的情況。isa<>
和cast<>
範本的範例是static bool isLoopInvariant(const Value *V, const Loop *L) { if (isa<Constant>(V) || isa<Argument>(V) || isa<GlobalValue>(V)) return true; // Otherwise, it must be an instruction... return !L->contains(cast<Instruction>(V)->getParent()); }
請注意,您不應使用
isa<>
測試,然後再使用cast<>
,對於這種情況,請使用dyn_cast<>
運算子。dyn_cast<>
:dyn_cast<>
運算子是“檢查轉換”操作。它檢查運算元是否為指定的類型,如果是,則傳回指向它的指標 (此運算子不適用於參考)。如果運算元不是正確的類型,則傳回空指標。因此,它的工作方式非常像 C++ 中的dynamic_cast<>
運算子,並且應該在相同的情況下使用。通常,dyn_cast<>
運算子在if
語句或類似的流程控制語句中使用,如下所示if (auto *AI = dyn_cast<AllocationInst>(Val)) { // ... }
這種形式的
if
語句有效地將對isa<>
的呼叫和對cast<>
的呼叫組合到一個語句中,這非常方便。請注意,
dyn_cast<>
運算子,就像 C++ 的dynamic_cast<>
或 Java 的instanceof
運算子一樣,可能會被濫用。特別是,您不應使用大的鏈式if/then/else
區塊來檢查大量不同類型的變體。如果您發現自己想要這樣做,那麼使用InstVisitor
類別直接分派指令類型會更清晰且更有效率。isa_and_present<>
:isa_and_present<>
運算子的工作方式與isa<>
運算子完全相同,只是它允許將空指標作為引數 (然後傳回 false)。這有時很有用,允許您將多個空值檢查合併為一個。cast_if_present<>
:cast_if_present<>
運算子的工作方式與cast<>
運算子完全相同,只是它允許將空指標作為引數 (然後傳播它)。這有時很有用,允許您將多個空值檢查合併為一個。dyn_cast_if_present<>
:dyn_cast_if_present<>
運算子的工作方式與dyn_cast<>
運算子完全相同,只是它允許將空指標作為引數 (然後傳播它)。這有時很有用,允許您將多個空值檢查合併為一個。
這五個範本可以用於任何類別,無論它們是否具有 v 表。如果您想新增對這些範本的支援,請參閱文件 如何為您的類別階層設定 LLVM 樣式的 RTTI
傳遞字串 ( StringRef
和 Twine
類別 )¶
雖然 LLVM 通常不進行太多字串操作,但我們確實有幾個重要的 API 接受字串。兩個重要的範例是 Value 類別 (它具有指令、函數等的名稱) 和 StringMap
類別,該類別在 LLVM 和 Clang 中被廣泛使用。
這些是通用類別,它們需要能夠接受可能具有嵌入式空字元的字串。因此,它們不能簡單地採用 const char *
,並且採用 const std::string&
需要用戶端執行堆積配置,這通常是不必要的。相反,許多 LLVM API 使用 StringRef
或 const Twine&
來有效率地傳遞字串。
StringRef
類別¶
StringRef
資料類型表示對常數字串 (字元陣列和長度) 的參考,並支援 std::string
上可用的常見操作,但不要求堆積配置。
它可以使用 C 樣式的以空字元結尾的字串、 std::string
隱式建構,或使用字元指標和長度顯式建構。例如, StringMap
find 函數宣告為
iterator find(StringRef Key);
用戶端可以使用以下任何一種方式呼叫它
Map.find("foo"); // Lookup "foo"
Map.find(std::string("bar")); // Lookup "bar"
Map.find(StringRef("\0baz", 4)); // Lookup "\0baz"
同樣,需要傳回字串的 API 可以傳回 StringRef
實例,該實例可以直接使用或使用 str
成員函數轉換為 std::string
。有關更多資訊,請參閱 llvm/ADT/StringRef.h
( doxygen )。
您應該很少直接使用 StringRef
類別,因為它包含指向外部記憶體的指標,通常儲存類別的實例是不安全的 (除非您知道外部儲存不會被釋放)。 StringRef
在 LLVM 中足夠小且普遍,它應該始終按值傳遞。
Twine
類別¶
Twine
( doxygen ) 類別是 API 接受串連字串的有效方式。例如,常見的 LLVM 範例是根據另一個指令的名稱加上字尾來命名一個指令,例如
New = CmpInst::Create(..., SO->getName() + ".cmp");
Twine
類別實際上是一個輕量級的 rope,它指向臨時 (堆疊配置) 物件。 Twine 可以隱式地建構為應用於字串的加號運算子的結果 (即,C 字串、 std::string
或 StringRef
)。 twine 會延遲字串的實際串連,直到實際需要它時才進行,此時它可以有效率地直接呈現到字元陣列中。這避免了在建構字串串連的臨時結果時涉及不必要的堆積配置。有關更多資訊,請參閱 llvm/ADT/Twine.h
( doxygen ) 和 這裡 。
與 StringRef
一樣, Twine
物件指向外部記憶體,幾乎永遠不應儲存或直接提及。它們僅用於定義應該能夠有效率地接受串連字串的函數時。
格式化字串 ( formatv
函數 )¶
雖然 LLVM 不一定進行大量的字串操作和剖析,但它確實進行了大量的字串格式化。從診斷訊息,到 llvm 工具輸出 (例如 llvm-readobj
),到列印詳細的反組譯清單和 LLDB 執行階段記錄,對字串格式化的需求非常普遍。
formatv
在精神上與 printf
相似,但它使用不同的語法,該語法大量借鑒了 Python 和 C#。與 printf
不同,它在編譯時推斷要格式化的類型,因此不需要格式規範 (例如 %d
)。這減少了嘗試建構可移植格式字串的腦力負擔,尤其是對於平台特定的類型 (例如 size_t
或指標類型)。與 printf
和 Python 不同,如果 LLVM 不知道如何格式化類型,它還會編譯失敗。這兩個屬性確保該函數比傳統的格式化方法 (例如 printf
函數系列) 更安全且更易於使用。
簡易格式化¶
對 formatv
的呼叫涉及單個格式字串,該字串由 0 個或多個替換序列組成,後跟可變長度的替換值列表。替換序列是 {N[[,align]:style]}
形式的字串。
N
指的是替換值列表中引數的從 0 開始的索引。請注意,這表示可以多次參考相同的參數,可能使用不同的樣式和/或對齊選項,並且順序不限。
align
是一個可選字串,用於指定格式化值的欄位寬度,以及值在欄位中的對齊方式。它被指定為可選的對齊樣式,後跟一個正整數欄位寬度。對齊樣式可以是字元 -
(左對齊)、 =
(置中對齊) 或 +
(右對齊) 之一。預設為右對齊。
style
是一個可選字串,由特定於類型的字串組成,用於控制值的格式化。例如,要將浮點值格式化為百分比,可以使用樣式選項 P
。
自訂格式化¶
有兩種方法可以自訂類型的格式化行為。
為您的類型
T
提供llvm::format_provider<T>
的範本特化,並提供適當的靜態 format 方法。
namespace llvm { template<> struct format_provider<MyFooBar> { static void format(const MyFooBar &V, raw_ostream &Stream, StringRef Style) { // Do whatever is necessary to format `V` into `Stream` } }; void foo() { MyFooBar X; std::string S = formatv("{0}", X); } }這是一個有用的擴展機制,用於為您自己的自訂類型新增格式化支援,並使用您自己的自訂樣式選項。但是,當您想要擴展機制來格式化庫已經知道如何格式化的類型時,它沒有幫助。為此,我們需要其他東西。
提供一個從
llvm::FormatAdapter<T>
繼承的 format 適配器。
namespace anything { struct format_int_custom : public llvm::FormatAdapter<int> { explicit format_int_custom(int N) : llvm::FormatAdapter<int>(N) {} void format(llvm::raw_ostream &Stream, StringRef Style) override { // Do whatever is necessary to format ``this->Item`` into ``Stream`` } }; } namespace llvm { void foo() { std::string S = formatv("{0}", anything::format_int_custom(42)); } }如果檢測到類型是從
FormatAdapter<T>
派生的,則formatv
將在引數上呼叫format
方法,並傳入指定的樣式。這允許人們提供任何類型的自訂格式化,包括已經具有內建 format 提供者的類型。
formatv
範例¶
以下旨在提供一組不完整的範例,示範 formatv
的用法。有關更多資訊,可以通過閱讀 doxygen 文件或查看單元測試套件來找到。
std::string S;
// Simple formatting of basic types and implicit string conversion.
S = formatv("{0} ({1:P})", 7, 0.35); // S == "7 (35.00%)"
// Out-of-order referencing and multi-referencing
outs() << formatv("{0} {2} {1} {0}", 1, "test", 3); // prints "1 3 test 1"
// Left, right, and center alignment
S = formatv("{0,7}", 'a'); // S == " a";
S = formatv("{0,-7}", 'a'); // S == "a ";
S = formatv("{0,=7}", 'a'); // S == " a ";
S = formatv("{0,+7}", 'a'); // S == " a";
// Custom styles
S = formatv("{0:N} - {0:x} - {1:E}", 12345, 123908342); // S == "12,345 - 0x3039 - 1.24E8"
// Adapters
S = formatv("{0}", fmt_align(42, AlignStyle::Center, 7)); // S == " 42 "
S = formatv("{0}", fmt_repeat("hi", 3)); // S == "hihihi"
S = formatv("{0}", fmt_pad("hi", 2, 6)); // S == " hi "
// Ranges
std::vector<int> V = {8, 9, 10};
S = formatv("{0}", make_range(V.begin(), V.end())); // S == "8, 9, 10"
S = formatv("{0:$[+]}", make_range(V.begin(), V.end())); // S == "8+9+10"
S = formatv("{0:$[ + ]@[x]}", make_range(V.begin(), V.end())); // S == "0x8 + 0x9 + 0xA"
錯誤處理¶
正確的錯誤處理有助於我們識別程式碼中的錯誤,並幫助終端使用者了解他們工具使用中的錯誤。錯誤分為兩大類: 程式錯誤 和 可恢復的錯誤,具有不同的處理和報告策略。
程式錯誤¶
程式錯誤是違反程式不變量或 API 合約的行為,表示程式本身存在錯誤。我們的目標是記錄不變量,並在運行時違反不變量時快速中止 (提供一些基本診斷)。
處理程式錯誤的基本工具是斷言和 llvm_unreachable 函數。斷言用於表達不變條件,並且應包含描述不變量的訊息
assert(isPhysReg(R) && "All virt regs should have been allocated already.");
llvm_unreachable 函數可用於記錄如果程式不變量成立則永遠不應輸入的控制流程區域
enum { Foo, Bar, Baz } X = foo();
switch (X) {
case Foo: /* Handle Foo */; break;
case Bar: /* Handle Bar */; break;
default:
llvm_unreachable("X should be Foo or Bar here");
}
可恢復的錯誤¶
可恢復的錯誤表示程式環境中的錯誤,例如資源故障 (缺少檔案、網路連線中斷等) 或格式錯誤的輸入。應該檢測到這些錯誤並將其傳達給程式中可以適當處理它們的層級。處理錯誤可能像向使用者報告問題一樣簡單,或者可能涉及嘗試恢復。
注意
雖然在整個 LLVM 中使用此錯誤處理方案是理想的,但在某些地方應用它並不實際。在您絕對必須發出非程式錯誤並且 Error
模型不可行的情况下,您可以呼叫 report_fatal_error
,它將呼叫已安裝的錯誤處理常式、列印訊息並中止程式。在這種情況下,不鼓勵使用 report_fatal_error 。
可恢復的錯誤使用 LLVM 的 Error
方案建模。此方案使用函數傳回值表示錯誤,類似於經典 C 整數錯誤碼或 C++ 的 std::error_code
。但是, Error
類別實際上是用戶定義錯誤類型的輕量級包裝器,允許附加任意資訊來描述錯誤。這類似於 C++ 例外允許拋出用戶定義類型的方式。
成功值通過呼叫 Error::success()
建立,例如
Error foo() {
// Do something.
// Return success.
return Error::success();
}
成功值的建構和傳回非常便宜 - 它們對程式效能的影響最小。
失敗值使用 make_error<T>
建構,其中 T
是任何從 ErrorInfo 實用程式繼承的類別,例如
class BadFileFormat : public ErrorInfo<BadFileFormat> {
public:
static char ID;
std::string Path;
BadFileFormat(StringRef Path) : Path(Path.str()) {}
void log(raw_ostream &OS) const override {
OS << Path << " is malformed";
}
std::error_code convertToErrorCode() const override {
return make_error_code(object_error::parse_failed);
}
};
char BadFileFormat::ID; // This should be declared in the C++ file.
Error printFormattedFile(StringRef Path) {
if (<check for valid format>)
return make_error<BadFileFormat>(Path);
// print file contents.
return Error::success();
}
錯誤值可以隱式轉換為布林值:true 表示錯誤,false 表示成功,啟用以下慣用語
Error mayFail();
Error foo() {
if (auto Err = mayFail())
return Err;
// Success! We can proceed.
...
對於可能會失敗但需要傳回值的功能,可以使用 Expected<T>
工具。此類型的值可以使用 T
或 Error
建構。 Expected<T>
值也可以隱式轉換為布林值,但與 Error
的慣例相反:成功為 true,錯誤為 false。如果成功,則可以使用取值運算符存取 T
值。如果失敗,則可以使用 takeError()
方法提取 Error
值。慣用的用法如下所示
Expected<FormattedFile> openFormattedFile(StringRef Path) {
// If badly formatted, return an error.
if (auto Err = checkFormat(Path))
return std::move(Err);
// Otherwise return a FormattedFile instance.
return FormattedFile(Path);
}
Error processFormattedFile(StringRef Path) {
// Try to open a formatted file
if (auto FileOrErr = openFormattedFile(Path)) {
// On success, grab a reference to the file and continue.
auto &File = *FileOrErr;
...
} else
// On error, extract the Error value and return it.
return FileOrErr.takeError();
}
如果 Expected<T>
值處於成功模式,則 takeError()
方法將傳回成功值。利用這個事實,上面的函數可以改寫為
Error processFormattedFile(StringRef Path) {
// Try to open a formatted file
auto FileOrErr = openFormattedFile(Path);
if (auto Err = FileOrErr.takeError())
// On error, extract the Error value and return it.
return Err;
// On success, grab a reference to the file and continue.
auto &File = *FileOrErr;
...
}
對於涉及多個 Expected<T>
值的函數,第二種形式通常更易於閱讀,因為它限制了所需的縮排。
如果 Expected<T>
值將被移動到現有的變數中,則 moveInto()
方法避免了命名額外變數的需求。這對於啟用 operator->()
很有用,因為 Expected<T>
值具有類似指標的語義。例如
Expected<std::unique_ptr<MemoryBuffer>> openBuffer(StringRef Path);
Error processBuffer(StringRef Buffer);
Error processBufferAtPath(StringRef Path) {
// Try to open a buffer.
std::unique_ptr<MemoryBuffer> MB;
if (auto Err = openBuffer(Path).moveInto(MB))
// On error, return the Error value.
return Err;
// On success, use MB.
return processBuffer(MB->getBuffer());
}
第三種形式適用於可以從 T&&
賦值的任何類型。如果 Expected<T>
值需要儲存在已宣告的 Optional<T>
中,這可能很有用。例如
Expected<StringRef> extractClassName(StringRef Definition);
struct ClassData {
StringRef Definition;
Optional<StringRef> LazyName;
...
Error initialize() {
if (auto Err = extractClassName(Path).moveInto(LazyName))
// On error, return the Error value.
return Err;
// On success, LazyName has been initialized.
...
}
};
所有 Error
實例,無論成功或失敗,都必須在銷毀之前進行檢查或從中移動(透過 std::move
或傳回)。意外丟棄未檢查的錯誤將導致程式在執行未檢查值的解構子時中止,從而可以輕鬆識別和修復違反此規則的情況。
成功值一旦經過測試(透過調用布林轉換運算符)就被視為已檢查
if (auto Err = mayFail(...))
return Err; // Failure value - move error to caller.
// Safe to continue: Err was checked.
相反地,即使 mayFail
傳回成功值,以下程式碼也總是會導致中止
mayFail();
// Program will always abort here, even if mayFail() returns Success, since
// the value is not checked.
失敗值一旦錯誤類型的處理程序被啟動,就被視為已檢查
handleErrors(
processFormattedFile(...),
[](const BadFileFormat &BFF) {
report("Unable to process " + BFF.Path + ": bad format");
},
[](const FileNotFound &FNF) {
report("File not found " + FNF.Path);
});
handleErrors
函數將錯誤作為其第一個參數,後跟可變參數列表的「處理程序」,每個處理程序都必須是具有一個參數的可調用類型(函數、lambda 或具有調用運算符的類別)。 handleErrors
函數將依序訪問每個處理程序,並根據錯誤的動態類型檢查其參數類型,並運行第一個匹配的處理程序。這與 C++ 異常用於決定運行哪個 catch 子句的決策過程相同。
由於傳遞給 handleErrors
的處理程序列表可能無法涵蓋可能發生的每種錯誤類型,因此 handleErrors
函數也會傳回一個必須檢查或傳播的 Error 值。如果傳遞給 handleErrors
的錯誤值與任何處理程序都不匹配,它將從 handleErrors 傳回。因此, handleErrors
的慣用用法如下所示
if (auto Err =
handleErrors(
processFormattedFile(...),
[](const BadFileFormat &BFF) {
report("Unable to process " + BFF.Path + ": bad format");
},
[](const FileNotFound &FNF) {
report("File not found " + FNF.Path);
}))
return Err;
在您確實知道處理程序列表是詳盡無遺的情況下,可以使用 handleAllErrors
函數來代替。這與 handleErrors
相同,只是如果傳入未處理的錯誤,它將終止程式,因此可以傳回 void。通常應避免使用 handleAllErrors
函數:在程式的其他地方引入新的錯誤類型很容易將以前詳盡無遺的錯誤列表變成非詳盡無遺的列表,從而導致意外的程式終止風險。在可能的情況下,請使用 handleErrors 並將未知錯誤向上傳播堆疊。
對於工具程式碼,在這種情況下,可以透過列印錯誤訊息然後以錯誤碼退出來處理錯誤, ExitOnError 工具可能是比 handleErrors 更好的選擇,因為它簡化了呼叫可能失敗函數時的控制流程。
在已知特定可能失敗函數的呼叫總是會成功的情況下(例如,呼叫僅在輸入子集上可能失敗的函數,且輸入已知是安全的),可以使用 cantFail 函數來移除錯誤類型,從而簡化控制流程。
StringError¶
許多類型的錯誤沒有復原策略,唯一可以採取的動作是將它們報告給使用者,以便使用者可以嘗試修復環境。在這種情況下,將錯誤表示為字串是完全合理的。 LLVM 提供了 StringError
類別來達到此目的。它接受兩個參數:字串錯誤訊息和等效的 std::error_code
以實現互操作性。它還提供了一個 createStringError
函數,以簡化此類別的常用用法
// These two lines of code are equivalent:
make_error<StringError>("Bad executable", errc::executable_format_error);
createStringError(errc::executable_format_error, "Bad executable");
如果您確定您正在建構的錯誤永遠不需要轉換為 std::error_code
,則可以使用 inconvertibleErrorCode()
函數
createStringError(inconvertibleErrorCode(), "Bad executable");
僅應在仔細考慮後才執行此操作。如果嘗試將此錯誤轉換為 std::error_code
,將立即觸發程式終止。除非您確定您的錯誤不需要互操作性,否則您應該尋找現有的 std::error_code
,您可以將其轉換為,甚至(儘管這很痛苦)考慮引入一個新的作為權宜之計。
createStringError
可以使用 printf
樣式的格式指定符來提供格式化的訊息
createStringError(errc::executable_format_error,
"Bad executable: %s", FileName);
與 std::error_code 和 ErrorOr 的互操作性¶
許多現有的 LLVM API 使用 std::error_code
及其夥伴 ErrorOr<T>
(其作用與 Expected<T>
相同,但包裝的是 std::error_code
而不是 Error
)。錯誤類型的傳染性意味著嘗試將其中一個函數更改為傳回 Error
或 Expected<T>
通常會導致呼叫者、呼叫者的呼叫者等等發生雪崩式的更改。(第一次這樣的嘗試,從 MachOObjectFile 的建構子傳回 Error
,在差異達到 3000 行,影響了六個程式庫,並且仍在增長後被放棄)。
為了解決這個問題,引入了 Error
/ std::error_code
互操作性要求。兩對函數允許將任何 Error
值轉換為 std::error_code
,將任何 Expected<T>
轉換為 ErrorOr<T>
,反之亦然
std::error_code errorToErrorCode(Error Err);
Error errorCodeToError(std::error_code EC);
template <typename T> ErrorOr<T> expectedToErrorOr(Expected<T> TOrErr);
template <typename T> Expected<T> errorOrToExpected(ErrorOr<T> TOrEC);
使用這些 API,可以輕鬆地進行手術式修補,將單個函數從 std::error_code
更新為 Error
,以及從 ErrorOr<T>
更新為 Expected<T>
。
從錯誤處理程序傳回錯誤¶
錯誤復原嘗試本身可能會失敗。因此, handleErrors
實際上識別了三種不同的處理程序簽名
// Error must be handled, no new errors produced:
void(UserDefinedError &E);
// Error must be handled, new errors can be produced:
Error(UserDefinedError &E);
// Original error can be inspected, then re-wrapped and returned (or a new
// error can be produced):
Error(std::unique_ptr<UserDefinedError> E);
從處理程序傳回的任何錯誤都將從 handleErrors
函數傳回,以便它可以自行處理,或向上傳播堆疊。
使用 ExitOnError 簡化工具程式碼¶
程式庫程式碼永遠不應針對可復原的錯誤呼叫 exit
,但是,在工具程式碼(尤其是命令列工具)中,這可能是一種合理的方法。遇到錯誤時呼叫 exit
會顯著簡化控制流程,因為不再需要將錯誤向上傳播堆疊。這允許以直線樣式編寫程式碼,只要每個可能失敗的呼叫都包裝在檢查和 exit 呼叫中即可。 ExitOnError
類別透過提供檢查 Error
值的呼叫運算符來支援此模式,在成功情況下剝離錯誤,並在失敗情況下記錄到 stderr
然後退出。
要使用此類別,請在您的程式中宣告全域 ExitOnError
變數
ExitOnError ExitOnErr;
然後,可以使用對 ExitOnErr
的呼叫來包裝對可能失敗函數的呼叫,將它們變成非失敗的呼叫
Error mayFail();
Expected<int> mayFail2();
void foo() {
ExitOnErr(mayFail());
int X = ExitOnErr(mayFail2());
}
在失敗時,錯誤的日誌訊息將被寫入 stderr
,您可以透過呼叫 setBanner 方法來選擇性地在其前面加上字串「橫幅」。也可以使用 setExitCodeMapper
方法提供從 Error
值到退出代碼的映射
int main(int argc, char *argv[]) {
ExitOnErr.setBanner(std::string(argv[0]) + " error:");
ExitOnErr.setExitCodeMapper(
[](const Error &Err) {
if (Err.isA<BadFileFormat>())
return 2;
return 1;
});
在您的工具程式碼中盡可能使用 ExitOnError
,因為它可以大大提高可讀性。
使用 cantFail 簡化安全調用站點¶
某些函數可能僅對其輸入的子集失敗,因此可以使用已知的安全輸入進行的呼叫可以假定為成功。
cantFail 函數透過包裝斷言(即它們的參數是成功值)來封裝此功能,並且在 Expected<T>
的情況下,解包 T 值
Error onlyFailsForSomeXValues(int X);
Expected<int> onlyFailsForSomeXValues2(int X);
void foo() {
cantFail(onlyFailsForSomeXValues(KnownSafeValue));
int Y = cantFail(onlyFailsForSomeXValues2(KnownSafeValue));
...
}
與 ExitOnError 工具程式類似, cantFail 簡化了控制流程。但是,它們對錯誤情況的處理非常不同:ExitOnError 保證在錯誤輸入時終止程式,而 cantFail 僅斷言結果是成功的。在偵錯組建中,如果遇到錯誤,這將導致斷言失敗。在發布組建中, cantFail 對失敗值的行為是未定義的。因此,在使用 cantFail 時必須謹慎:客戶端必須確定 cantFail 包裝的呼叫確實不會因給定的參數而失敗。
在程式庫程式碼中應很少使用 cantFail 函數,但是它們可能在工具和單元測試程式碼中更有用,在這些程式碼中,輸入和/或模擬的類別或函數可能已知是安全的。
可能失敗的建構子¶
某些類別需要資源獲取或其他複雜的初始化,這些初始化在建構期間可能會失敗。不幸的是,建構子無法傳回錯誤,並且讓客戶端在建構物件後測試物件以確保它們有效是容易出錯的,因為太容易忘記測試。為了解決這個問題,請使用命名建構子慣用語並傳回 Expected<T>
class Foo {
public:
static Expected<Foo> Create(Resource R1, Resource R2) {
Error Err = Error::success();
Foo F(R1, R2, Err);
if (Err)
return std::move(Err);
return std::move(F);
}
private:
Foo(Resource R1, Resource R2, Error &Err) {
ErrorAsOutParameter EAO(&Err);
if (auto Err2 = R1.acquire()) {
Err = std::move(Err2);
return;
}
Err = R2.acquire();
}
};
在這裡,命名建構子將 Error
透過參考傳遞到實際的建構子中,然後建構子可以使用它來傳回錯誤。 ErrorAsOutParameter
工具在進入建構子時設定 Error
值的已檢查標誌,以便可以將錯誤賦值給它,然後在退出時重置它,以強制客戶端(命名建構子)檢查錯誤。
透過使用這種慣用語,嘗試建構 Foo 的客戶端會收到格式良好的 Foo 或 Error,永遠不會收到處於無效狀態的物件。
基於類型傳播和消耗錯誤¶
在某些情況下,已知某些類型的錯誤是良性的。例如,當遍歷封存檔時,某些客戶端可能很樂意跳過格式錯誤的物件檔案,而不是立即終止遍歷。跳過格式錯誤的物件可以使用精細的處理程序方法來實現,但是 Error.h 標頭提供了兩個實用工具,使這種慣用語更加簡潔:類型檢查方法 isA
和 consumeError
函數
Error walkArchive(Archive A) {
for (unsigned I = 0; I != A.numMembers(); ++I) {
auto ChildOrErr = A.getMember(I);
if (auto Err = ChildOrErr.takeError()) {
if (Err.isA<BadFileFormat>())
consumeError(std::move(Err))
else
return Err;
}
auto &Child = *ChildOrErr;
// Use Child
...
}
return Error::success();
}
使用 joinErrors 連接錯誤¶
在上面的封存檔遍歷範例中, BadFileFormat
錯誤只是被消耗和忽略。如果客戶端想要在完成遍歷封存檔後報告這些錯誤,他們可以使用 joinErrors
工具
Error walkArchive(Archive A) {
Error DeferredErrs = Error::success();
for (unsigned I = 0; I != A.numMembers(); ++I) {
auto ChildOrErr = A.getMember(I);
if (auto Err = ChildOrErr.takeError())
if (Err.isA<BadFileFormat>())
DeferredErrs = joinErrors(std::move(DeferredErrs), std::move(Err));
else
return Err;
auto &Child = *ChildOrErr;
// Use Child
...
}
return DeferredErrs;
}
joinErrors
例程建構一個名為 ErrorList
的特殊錯誤類型,它保存使用者定義錯誤的列表。 handleErrors
例程識別此類型,並將嘗試依序處理包含的每個錯誤。如果可以處理所有包含的錯誤,則 handleErrors
將傳回 Error::success()
,否則 handleErrors
將連接剩餘的錯誤並傳回結果 ErrorList
。
建構可能失敗的迭代器和迭代器範圍¶
上面的封存檔遍歷範例透過索引檢索封存檔成員,但是這需要大量的樣板程式碼來進行迭代和錯誤檢查。我們可以透過使用「可能失敗的迭代器」模式來簡化此操作,該模式支援以下用於可能失敗的容器(如 Archive)的自然迭代慣用語
Error Err = Error::success();
for (auto &Child : Ar->children(Err)) {
// Use Child - only enter the loop when it's valid
// Allow early exit from the loop body, since we know that Err is success
// when we're inside the loop.
if (BailOutOn(Child))
return;
...
}
// Check Err after the loop to ensure it didn't break due to an error.
if (Err)
return Err;
為了啟用此慣用語,遍歷可能失敗容器的迭代器以自然樣式編寫,其 ++
和 --
運算符替換為可能失敗的 Error inc()
和 Error dec()
函數。例如
class FallibleChildIterator {
public:
FallibleChildIterator(Archive &A, unsigned ChildIdx);
Archive::Child &operator*();
friend bool operator==(const ArchiveIterator &LHS,
const ArchiveIterator &RHS);
// operator++/operator-- replaced with fallible increment / decrement:
Error inc() {
if (!A.childValid(ChildIdx + 1))
return make_error<BadArchiveMember>(...);
++ChildIdx;
return Error::success();
}
Error dec() { ... }
};
然後,這種可能失敗的迭代器介面的實例會使用 fallible_iterator 工具程式進行包裝,該工具程式提供 operator++
和 operator--
,透過在建構時傳遞給封裝器的參考來傳回任何錯誤。 fallible_iterator 封裝器負責 (a) 在發生錯誤時跳到範圍的末尾,以及 (b) 每當迭代器與 end
進行比較且發現不相等時,將錯誤標記為已檢查(特別是:這會在基於範圍的 for 迴圈的整個主體中將錯誤標記為已檢查),從而實現從迴圈提前退出,而無需冗餘的錯誤檢查。
可能失敗的迭代器介面的實例(例如上面的 FallibleChildIterator)使用 make_fallible_itr
和 make_fallible_end
函數進行包裝。例如
class Archive {
public:
using child_iterator = fallible_iterator<FallibleChildIterator>;
child_iterator child_begin(Error &Err) {
return make_fallible_itr(FallibleChildIterator(*this, 0), Err);
}
child_iterator child_end() {
return make_fallible_end(FallibleChildIterator(*this, size()));
}
iterator_range<child_iterator> children(Error &Err) {
return make_range(child_begin(Err), child_end());
}
};
使用 fallible_iterator 工具程式可以自然地建構可能失敗的迭代器(使用失敗的 inc
和 dec
操作)以及相對自然地使用 c++ 迭代器/迴圈慣用語。
有關 Error 及其相關工具程式的更多資訊,請參閱 Error.h 標頭檔。
傳遞函數和其他可調用物件¶
有時您可能希望將回呼物件傳遞給函數。為了支援 lambda 表達式和其他函數物件,您不應使用傳統的 C 方法,即採用函數指標和不透明的 cookie
void takeCallback(bool (*Callback)(Function *, void *), void *Cookie);
相反,請使用以下方法之一
函數模板¶
如果您不介意將函數的定義放在標頭檔中,請將其設為函數模板,該模板以可調用類型為模板。
template<typename Callable>
void takeCallback(Callable Callback) {
Callback(1, 2, 3);
}
function_ref
類別模板¶
function_ref
(doxygen)類別模板表示對可調用物件的參考,該模板以可調用物件的類型為模板。如果您不需要在函數傳回後保留回呼,則這是將回呼傳遞給函數的好選擇。這樣, function_ref
之於 std::function
,就像 StringRef
之於 std::string
。
function_ref<Ret(Param1, Param2, ...)>
可以從任何可調用物件隱式建構,該物件可以使用類型為 Param1
、 Param2
、 ... 的參數調用,並傳回可以轉換為類型 Ret
的值。例如
void visitBasicBlocks(Function *F, function_ref<bool (BasicBlock*)> Callback) {
for (BasicBlock &BB : *F)
if (Callback(&BB))
return;
}
可以使用以下方式調用
visitBasicBlocks(F, [&](BasicBlock *BB) {
if (process(BB))
return isEmpty(BB);
return false;
});
請注意, function_ref
物件包含指向外部記憶體的指標,因此通常儲存類別的實例是不安全的(除非您知道外部儲存空間不會被釋放)。如果您需要此功能,請考慮使用 std::function
。 function_ref
足夠小,應該始終按值傳遞。
LLVM_DEBUG()
巨集和 -debug
選項¶
通常,在處理您的 pass 時,您會將一堆偵錯列印和其他程式碼放入您的 pass 中。在您使其工作後,您想要移除它,但是您將來可能再次需要它(以解決您遇到的新錯誤)。
自然地,因此,您不想刪除偵錯列印,但您不希望它們總是嘈雜的。標準的折衷方案是註解掉它們,允許您在將來需要它們時啟用它們。
llvm/Support/Debug.h
(doxygen)檔案提供了一個名為 LLVM_DEBUG()
的巨集,這是此問題的更好解決方案。基本上,您可以將任意程式碼放入 LLVM_DEBUG
巨集的參數中,並且僅當使用「 -debug
」命令列參數運行「 opt
」(或任何其他工具)時才會執行它
LLVM_DEBUG(dbgs() << "I am here!\n");
然後您可以像這樣運行您的 pass
$ opt < a.bc > /dev/null -mypass
<no output>
$ opt < a.bc > /dev/null -mypass -debug
I am here!
使用 LLVM_DEBUG()
巨集而不是自製的解決方案,您可以不必為您的 pass 的偵錯輸出建立「另一個」命令列選項。請注意, LLVM_DEBUG()
巨集在非斷言組建中被禁用,因此它們根本不會造成效能影響(出於同樣的原因,它們也不應包含副作用!)。
關於 LLVM_DEBUG()
巨集的另一個好處是,您可以直接在 gdb 中啟用或禁用它。只需從 gdb 使用「 set DebugFlag=0
」或「 set DebugFlag=1
」(如果程式正在運行)。如果程式尚未啟動,您始終可以使用 -debug
運行它。
使用 DEBUG_TYPE
和 -debug-only
選項進行細粒度偵錯資訊¶
有時您可能會發現自己處於啟用 -debug
只會開啟太多資訊的情況(例如在處理程式碼產生器時)。如果您想要啟用具有更細粒度控制的偵錯資訊,則應定義 DEBUG_TYPE
巨集並使用 -debug-only
選項,如下所示
#define DEBUG_TYPE "foo"
LLVM_DEBUG(dbgs() << "'foo' debug type\n");
#undef DEBUG_TYPE
#define DEBUG_TYPE "bar"
LLVM_DEBUG(dbgs() << "'bar' debug type\n");
#undef DEBUG_TYPE
然後您可以像這樣運行您的 pass
$ opt < a.bc > /dev/null -mypass
<no output>
$ opt < a.bc > /dev/null -mypass -debug
'foo' debug type
'bar' debug type
$ opt < a.bc > /dev/null -mypass -debug-only=foo
'foo' debug type
$ opt < a.bc > /dev/null -mypass -debug-only=bar
'bar' debug type
$ opt < a.bc > /dev/null -mypass -debug-only=foo,bar
'foo' debug type
'bar' debug type
當然,在實務中,您應該僅在檔案的頂部設定 DEBUG_TYPE
,以指定整個模組的偵錯類型。請注意,您僅應在包含 Debug.h 之後執行此操作,而不是在任何標頭的 #include 周圍執行。此外,您應該使用比「foo」和「bar」更有意義的名稱,因為沒有系統可以確保名稱不衝突。如果兩個不同的模組使用相同的字串,則當指定名稱時,它們都將被開啟。例如,這允許使用 -debug-only=InstrSched
啟用指令調度的所有偵錯資訊,即使來源位於多個檔案中。名稱不得包含逗號 (,),因為逗號用於分隔 -debug-only
選項的參數。
出於效能原因,-debug-only 在 LLVM 的最佳化組建( --enable-optimized
)中不可用。
DEBUG_WITH_TYPE
巨集也可用於您想要設定 DEBUG_TYPE
但僅針對一個特定的 DEBUG
語句的情況。它採用一個額外的第一個參數,即要使用的類型。例如,前面的範例可以寫成
DEBUG_WITH_TYPE("foo", dbgs() << "'foo' debug type\n");
DEBUG_WITH_TYPE("bar", dbgs() << "'bar' debug type\n");
Statistic
類別 & -stats
選項¶
llvm/ADT/Statistic.h
(doxygen)檔案提供了一個名為 Statistic
的類別,該類別用作追蹤 LLVM 編譯器正在執行的操作以及各種優化效果的統一方法。了解哪些優化有助於使特定程式執行更快很有用。
通常,您可能會在一些大型程式上運行您的 pass,並且您有興趣了解它進行特定轉換的次數。儘管您可以使用手動檢查或一些臨時方法來完成此操作,但這確實很麻煩,並且對於大型程式來說不是很有用。使用 Statistic
類別可以非常輕鬆地追蹤此資訊,並且計算出的資訊以與正在執行的其餘 pass 統一的方式呈現。
有很多 Statistic
用法的範例,但是使用它的基本知識如下
像這樣定義您的統計資訊
#define DEBUG_TYPE "mypassname" // This goes after any #includes.
STATISTIC(NumXForms, "The # of times I did stuff");
STATISTIC
巨集定義了一個靜態變數,其名稱由第一個參數指定。 pass 名稱取自 DEBUG_TYPE
巨集,描述取自第二個參數。定義的變數(在本例中為「NumXForms」)的作用類似於無符號整數。
每當您進行轉換時,增加計數器
++NumXForms; // I did stuff!
這就是您必須做的全部。要讓「 opt
」列印出收集的統計資訊,請使用「 -stats
」選項
$ opt -stats -mypassname < program.bc > /dev/null
... statistics output ...
請注意,為了使用「 -stats
」選項,LLVM 必須在啟用斷言的情況下編譯。
當在 SPEC 基準測試套件中的 C 檔案上運行 opt
時,它會提供如下所示的報告
7646 bitcodewriter - Number of normal instructions
725 bitcodewriter - Number of oversized instructions
129996 bitcodewriter - Number of bitcode bytes written
2817 raise - Number of insts DCEd or constprop'd
3213 raise - Number of cast-of-self removed
5046 raise - Number of expression trees converted
75 raise - Number of other getelementptr's formed
138 raise - Number of load/store peepholes
42 deadtypeelim - Number of unused typenames removed from symtab
392 funcresolve - Number of varargs functions resolved
27 globaldce - Number of global variables removed
2 adce - Number of basic blocks removed
134 cee - Number of branches revectored
49 cee - Number of setcc instruction eliminated
532 gcse - Number of loads removed
2919 gcse - Number of instructions removed
86 indvars - Number of canonical indvars added
87 indvars - Number of aux indvars removed
25 instcombine - Number of dead inst eliminate
434 instcombine - Number of insts combined
248 licm - Number of load insts hoisted
1298 licm - Number of insts hoisted to a loop pre-header
3 licm - Number of insts hoisted to multiple loop preds (bad, no loop pre-header)
75 mem2reg - Number of alloca's promoted
1444 cfgsimplify - Number of blocks simplified
顯然,由於有如此多的優化,因此擁有此類東西的統一框架非常好。使您的 pass 很好地適應框架使其更具可維護性和實用性。
新增偵錯計數器以協助偵錯您的程式碼¶
有時,在編寫新的 pass 或嘗試追蹤錯誤時,能夠控制 pass 中的某些內容是否發生很有用。例如,有時最小化工具只能輕鬆地為您提供大型測試案例。您希望使用二分法自動將您的錯誤縮小到特定轉換是否正在發生。
llvm/Support/DebugCounter.h
(doxygen)檔案提供了一個名為 DebugCounter
的類別,該類別可用於建立命令列計數器選項,以控制程式碼部分的執行。
像這樣定義您的 DebugCounter
DEBUG_COUNTER(DeleteAnInstruction, "passname-delete-instruction",
"Controls which instructions get delete");
DEBUG_COUNTER
巨集定義了一個靜態變數,其名稱由第一個參數指定。計數器的名稱(在命令列上使用)由第二個參數指定,並且說明中使用的描述由第三個參數指定。
無論您想要控制什麼程式碼,都使用 DebugCounter::shouldExecute
來控制它。
if (DebugCounter::shouldExecute(DeleteAnInstruction))
I->eraseFromParent();
這就是您必須做的全部。現在,使用 opt,您可以使用「 --debug-counter
」選項來控制何時觸發此程式碼。指定何時執行程式碼路徑。
$ opt --debug-counter=passname-delete-instruction=2-3 -passname
這會在第一次執行到上述程式碼時跳過它,第二次執行時也跳過,然後執行 2 次,接著跳過剩餘的執行。
所以如果對以下程式碼執行:
%1 = add i32 %a, %b
%2 = add i32 %a, %b
%3 = add i32 %a, %b
%4 = add i32 %a, %b
它會刪除數字 %2
和 %3
。
在 utils/bisect-skip-count 中提供了一個工具,用於二元搜尋範圍參數的起始和結束位置。它可以用於自動最小化除錯計數器變數的範圍。
在 llvm/tools/reduce-chunk-list/reduce-chunk-list.cpp 中提供了一個更通用的工具,用於最小化除錯計數器區塊列表。
如何使用 reduce-chunk-list:首先,找出您想要最小化的除錯計數器的呼叫次數。為此,執行您想要最小化的編譯命令,並加入 -print-debug-counter,如果需要,請加入 -mllvm。然後找到包含您感興趣的計數器的行。它應該看起來像這樣:
my-counter : {5678,empty}
呼叫 my-counter 的次數是 5678
然後使用 reduce-chunk-list 找出最小的有趣區塊集合。建立一個重現腳本,例如:
#! /bin/bash
opt -debug-counter=my-counter=$1
# ... Test result of the command. Failure of the script is considered interesting
然後執行 reduce-chunk-list my-script.sh 0-5678 2>&1 | tee dump_bisect 這個命令可能需要一些時間。但完成後,它會印出類似以下的結果:Minimal Chunks = 0:1:5:11-12:33-34
除錯程式碼時檢視圖形¶
LLVM 中幾個重要的資料結構是圖形:例如由 LLVM BasicBlocks 組成的 CFG、由 LLVM MachineBasicBlocks 組成的 CFG,以及 Instruction Selection DAGs。在許多情況下,當除錯編譯器的各個部分時,能夠立即視覺化這些圖形會很有幫助。
LLVM 提供了幾個在除錯組建中可用的回呼函式,正是為了做到這一點。如果您呼叫 Function::viewCFG()
方法,例如,目前的 LLVM 工具會彈出一個視窗,其中包含該函式的 CFG,其中每個基本區塊都是圖形中的一個節點,而每個節點都包含該區塊中的指令。類似地,也存在 Function::viewCFGOnly()
(不包含指令)、MachineFunction::viewCFG()
和 MachineFunction::viewCFGOnly()
,以及 SelectionDAG::viewGraph()
方法。例如,在 GDB 中,您通常可以使用類似 call DAG.viewGraph()
的指令來彈出視窗。或者,您可以將對這些函式的呼叫散佈在您想要除錯的程式碼位置。
要讓這個功能運作,需要少量設定。在具有 X11 的 Unix 系統上,安裝 graphviz 工具組,並確保 ‘dot’ 和 ‘gv’ 在您的路徑中。如果您在 macOS 上執行,請下載並安裝 macOS Graphviz 程式,並將 /Applications/Graphviz.app/Contents/MacOS/
(或您安裝的位置)加入到您的路徑中。這些程式在設定、建置或執行 LLVM 時不需要存在,只需在需要時於活動除錯會期期間安裝即可。
SelectionDAG
已擴充,使其更容易在大型複雜圖形中找到感興趣的節點。從 gdb 中,如果您 call DAG.setGraphColor(node, "color")
,則下一次 call DAG.viewGraph()
將以指定的顏色突出顯示該節點(顏色的選擇可以在 colors 中找到)。可以使用 call DAG.setGraphAttrs(node, "attributes")
提供更複雜的節點屬性(選擇可以在 Graph attributes 中找到)。如果您想要重新開始並清除所有目前的圖形屬性,則可以使用 call DAG.clearGraphAttrs()
。
請注意,圖形視覺化功能已從發布版本中編譯移除,以縮減檔案大小。這表示您需要除錯 + 斷言或發布 + 斷言組建才能使用這些功能。
為任務挑選正確的資料結構¶
LLVM 在 llvm/ADT/
目錄中擁有大量的資料結構,而且我們經常使用 STL 資料結構。本節描述您在挑選其中一種時應考量的取捨。
第一步是選擇您自己的冒險:您想要循序容器、類似集合的容器,還是類似映射的容器?選擇容器時,最重要的是您計劃如何存取容器的演算法特性。基於此,您應該使用:
如果您需要基於另一個值有效率地查找值,則使用類似映射的容器。類似映射的容器也支援有效率地查詢是否包含(鍵是否在映射中)。類似映射的容器通常不支援有效率的反向映射(值到鍵)。如果您需要這樣做,請使用兩個映射。某些類似映射的容器也支援有效率地迭代排序後的鍵。類似映射的容器是最昂貴的類型,只有在您需要這些功能之一時才使用它們。
如果您需要將一堆東西放入自動消除重複項的容器中,則使用類似集合的容器。某些類似集合的容器支援有效率地迭代排序後的元素。類似集合的容器比循序容器更昂貴。
如果您需要以最有效率的方式新增元素,並追蹤元素新增到集合的順序,則使用循序容器。它們允許重複項並支援有效率的迭代,但不支援基於鍵的有效率查找。
如果您需要用於字元或位元組陣列的專用循序容器或參考結構,則使用字串容器。
如果您需要有效率地儲存和對數字 ID 集合執行集合運算,同時自動消除重複項,則使用位元容器。位元容器對於您想要儲存的每個識別碼最多需要 1 個位元。
一旦確定了容器的正確類別,您就可以透過明智地挑選該類別的成員來微調記憶體使用量、常數因子和存取的快取行為。請注意,常數因子和快取行為可能非常重要。例如,如果您有一個通常只包含少量元素(但可能包含許多元素)的向量,則最好使用 SmallVector 而不是 vector。這樣做可以避免(相對)昂貴的 malloc/free 呼叫,這些呼叫會使將元素新增到容器的成本相形見絀。
循序容器(std::vector、std::list 等)¶
根據您的需求,有多種循序容器可供您使用。在本節中挑選第一個可以滿足您需求的容器。
llvm/ADT/ArrayRef.h¶
llvm::ArrayRef
類別是在介面中接受記憶體中循序元素列表並僅從中讀取時,偏好的類別。透過採用 ArrayRef
,API 可以傳遞固定大小的陣列、std::vector
、llvm::SmallVector
以及任何在記憶體中連續的東西。
固定大小陣列¶
固定大小的陣列非常簡單且非常快速。如果您確切知道您有多少個元素,或者您對您擁有的元素數量有一個(較低的)上限,則它們很好用。
堆積分配陣列¶
堆積分配陣列(new[]
+ delete[]
)也很簡單。如果元素數量可變、如果您在陣列分配之前就知道您需要多少個元素,並且如果陣列通常很大(如果不是,請考慮使用 SmallVector),則它們很好用。堆積分配陣列的成本是 new/delete(又稱 malloc/free)的成本。另請注意,如果您要分配具有建構子的類型陣列,則會為陣列中的每個元素執行建構子和解構子(可調整大小的向量僅建構實際使用的元素)。
llvm/ADT/TinyPtrVector.h¶
TinyPtrVector<Type>
是一個高度專業化的集合類別,經過最佳化以避免在向量具有零個或一個元素的情況下進行分配。它有兩個主要限制:1) 它只能保存指標類型的值,以及 2) 它不能保存空指標。
由於此容器高度專業化,因此很少使用。
llvm/ADT/SmallVector.h¶
SmallVector<Type, N>
是一個簡單的類別,看起來和聞起來就像 vector<Type>
:它支援有效率的迭代,以記憶體順序配置元素(因此您可以在元素之間進行指標運算),支援有效率的 push_back/pop_back 操作,支援對其元素的有效率隨機存取等等。
SmallVector 的主要優點是它在物件本身中為一些元素 (N) 分配空間。因此,如果 SmallVector 在動態上小於 N,則不會執行 malloc。在 malloc/free 呼叫遠比擺弄元素程式碼更昂貴的情況下,這可能是一個很大的優勢。
這適用於「通常很小」的向量(例如,區塊的前導/後繼數量通常小於 8)。另一方面,這使得 SmallVector 本身的大小很大,因此您不希望分配大量的 SmallVector(這樣做會浪費大量空間)。因此,SmallVector 在堆疊上最有用。
在沒有充分動機選擇內嵌元素數量 N
的情況下,建議使用 SmallVector<T>
(即省略 N
)。這將選擇預設數量的內嵌元素,這些元素對於在堆疊上分配是合理的(例如,嘗試保持 sizeof(SmallVector<T>)
在 64 位元組左右)。
SmallVector 也為 alloca
提供了良好的可移植且有效率的替代方案。
SmallVector 相較於 std::vector 而言,也發展出了一些其他小的優勢,導致 SmallVector<Type, 0>
比 std::vector<Type>
更受青睞。
std::vector 是例外安全的,而且某些實作具有悲觀化,會在 SmallVector 移動元素時複製元素。
SmallVector 理解
std::is_trivially_copyable<Type>
並積極使用 realloc。許多 LLVM API 採用 SmallVectorImpl 作為 out 參數(請參閱下面的註解)。
N 等於 0 的 SmallVector 在 64 位元平台上比 std::vector 小,因為它將
unsigned
(而不是void*
)用於其大小和容量。
注意
偏好使用 ArrayRef<T>
或 SmallVectorImpl<T>
作為參數類型。
將 SmallVector<T, N>
作為參數類型很少是合適的。如果 API 僅從向量讀取,則應使用 ArrayRef。即使 API 更新向量,「小尺寸」也可能不相關;這樣的 API 應使用 SmallVectorImpl<T>
類別,它是「向量標頭」(和方法),但不包含在其後分配的元素。請注意,SmallVector<T, N>
繼承自 SmallVectorImpl<T>
,因此轉換是隱含的且不花費任何成本。例如:
// DISCOURAGED: Clients cannot pass e.g. raw arrays.
hardcodedContiguousStorage(const SmallVectorImpl<Foo> &In);
// ENCOURAGED: Clients can pass any contiguous storage of Foo.
allowsAnyContiguousStorage(ArrayRef<Foo> In);
void someFunc1() {
Foo Vec[] = { /* ... */ };
hardcodedContiguousStorage(Vec); // Error.
allowsAnyContiguousStorage(Vec); // Works.
}
// DISCOURAGED: Clients cannot pass e.g. SmallVector<Foo, 8>.
hardcodedSmallSize(SmallVector<Foo, 2> &Out);
// ENCOURAGED: Clients can pass any SmallVector<Foo, N>.
allowsAnySmallSize(SmallVectorImpl<Foo> &Out);
void someFunc2() {
SmallVector<Foo, 8> Vec;
hardcodedSmallSize(Vec); // Error.
allowsAnySmallSize(Vec); // Works.
}
即使它的名稱中有「Impl
」,SmallVectorImpl 也被廣泛使用,而且不再是「實作私有的」。像 SmallVectorHeader
這樣的名稱可能更合適。
llvm/ADT/PagedVector.h¶
PagedVector<Type, PageSize>
是一個隨機存取容器,當透過 operator[]
存取頁面的第一個元素時,它會分配 PageSize
個 Type
類型的元素。這適用於預先知道元素數量的情況;它們的實際初始化成本很高;而且它們很少使用。當元素被存取時,此工具使用頁面粒度的延遲初始化。當使用的頁面數量很少時,可以實現顯著的記憶體節省。
主要優點是 PagedVector
允許延遲頁面的實際分配,直到需要時才分配,但代價是每個頁面一個指標,以及透過其位置索引存取元素時的一個額外間接層級。
為了最小化此容器的記憶體佔用空間,平衡 PageSize 非常重要,使其既不太小(否則每個頁面的指標開銷可能變得太高),也不太大(否則如果頁面未完全使用,則會浪費記憶體)。
此外,雖然保留了元素基於其插入索引的順序,就像向量一樣,但由於按順序存取元素會分配所有迭代的頁面,從而破壞記憶體節省和 PagedVector
的目的,因此 API 中未提供透過 begin()
和 end()
迭代元素。
最後,提供了 materialized_begin()
和 materialized_end
迭代器,用於存取與已存取頁面相關聯的元素,這可以加速需要以非排序方式迭代初始化元素的操作。
<vector>¶
std::vector<T>
廣受喜愛和尊重。但是,由於上面列出的優點,SmallVector<T, 0>
通常是更好的選擇。當您需要儲存超過 UINT32_MAX
個元素或與期望向量的代码介面時,std::vector 仍然很有用:)。
關於 std::vector 一個值得注意的事項:避免像這樣的程式碼
for ( ... ) {
std::vector<foo> V;
// make use of V.
}
相反,將其寫成這樣
std::vector<foo> V;
for ( ... ) {
// make use of V.
V.clear();
}
這樣做將在迴圈的每次迭代中節省(至少)一次堆積分配和釋放。
<deque>¶
std::deque
在某些方面是 std::vector
的廣義版本。與 std::vector
類似,它提供常數時間的隨機存取和其他類似的屬性,但它也提供對列表前端的有效率存取。它不保證記憶體中元素的連續性。
為了換取這種額外的彈性,std::deque
的常數因子成本比 std::vector
高得多。如果可能,請使用 std::vector
或更便宜的東西。
<list>¶
std::list
是一個效率極低的類別,很少有用。它為插入其中的每個元素執行堆積分配,因此具有極高的常數因子,尤其是對於小型資料類型。std::list
也僅支援雙向迭代,而不支援隨機存取迭代。
為了換取這種高成本,std::list 支援有效率地存取列表的兩端(類似於 std::deque
,但不像 std::vector
或 SmallVector
)。此外,std::list 的迭代器失效特性比向量類別更強:將元素插入或移除到列表中不會使列表中其他元素的迭代器或指標失效。
llvm/ADT/ilist.h¶
ilist<T>
實作了一個「侵入式」雙向鏈結列表。它是侵入式的,因為它需要元素儲存並提供對列表的 prev/next 指標的存取。
ilist
具有與 std::list
相同的缺點,並且還需要元素類型的 ilist_traits
實作,但它提供了一些新穎的特性。特別是,它可以有效率地儲存多型物件,當元素從列表中插入或移除時,會通知 traits 類別,並且保證 ilist
支援常數時間的拼接操作。
ilist
和 iplist
是彼此的 using
別名,後者目前僅因歷史原因而存在。
這些屬性正是我們對 Instruction
和基本區塊等事物所需要的,這就是為什麼這些是使用 ilist
實作的原因。
相關的感興趣類別將在以下小節中說明
llvm/ADT/PackedVector.h¶
適用於儲存值向量,每個值僅使用少數位元。除了類似向量容器的標準操作外,它還可以執行「或」集合運算。
例如
enum State {
None = 0x0,
FirstCondition = 0x1,
SecondCondition = 0x2,
Both = 0x3
};
State get() {
PackedVector<State, 2> Vec1;
Vec1.push_back(FirstCondition);
PackedVector<State, 2> Vec2;
Vec2.push_back(SecondCondition);
Vec1 |= Vec2;
return Vec1[0]; // returns 'Both'.
}
ilist_traits¶
ilist_traits<T>
是 ilist<T>
的自訂機制。ilist<T>
公開衍生自此 traits 類別。
llvm/ADT/ilist_node.h¶
ilist_node<T>
以預設方式實作 ilist<T>
(和類似容器)預期的向前和向後連結。
ilist_node<T>
旨在嵌入節點類型 T
中,通常 T
公開衍生自 ilist_node<T>
。
哨兵¶
ilist
還有另一個必須考量的特殊性。為了成為 C++ 生態系統中的良好公民,它需要支援標準容器操作,例如 begin
和 end
迭代器等。此外,在非空 ilist
的情況下,operator--
必須在 end
迭代器上正確運作。
此問題唯一合理的解決方案是與侵入式列表一起分配一個所謂的哨兵,它充當 end
迭代器,提供到最後一個元素的後向連結。然而,根據 C++ 慣例,在哨兵之後進行 operator++
是非法的,而且也不得取消參考。
這些限制允許 ilist
在如何分配和儲存哨兵方面具有一些實作自由度。對應的策略由 ilist_traits<T>
決定。預設情況下,每當需要哨兵時,都會對 T
進行堆積分配。
雖然預設策略在大多數情況下都足夠,但當 T
未提供預設建構子時,它可能會崩潰。此外,在許多 ilist
實例的情況下,相關哨兵的記憶體開銷會被浪費。為了緩解眾多且大量的 T
哨兵的情況,有時會採用一種技巧,導致幽靈哨兵。
幽靈哨兵是透過特別設計的 ilist_traits<T>
獲得的,它將哨兵與記憶體中的 ilist
實例疊加。指標運算用於取得相對於 ilist
的 this
指標的哨兵。ilist
透過額外的指標進行擴充,該指標充當哨兵的後向連結。這是幽靈哨兵中唯一可以合法存取的欄位。
其他循序容器選項¶
其他 STL 容器可用,例如 std::string
。
還有各種 STL 配接器類別,例如 std::queue
、std::priority_queue
、std::stack
等。這些提供了對底層容器的簡化存取,但不影響容器本身的成本。
類似字串的容器¶
在 C 和 C++ 中,有多種方法可以傳遞和使用字串,而 LLVM 新增了一些新的選項可供選擇。在此列表中挑選第一個可以滿足您需求的選項,它們根據其相對成本排序。
請注意,通常偏好不要將字串作為 const char*
傳遞。這些字串存在許多問題,包括它們無法表示嵌入的空字元(“0”),並且無法有效率地取得長度。 ‘const char*
’ 的一般替代方案是 StringRef。
有關為 API 選擇字串容器的更多資訊,請參閱 傳遞字串。
llvm/ADT/StringRef.h¶
StringRef 類別是一個簡單的值類別,包含指向字元的指標和長度,並且與 ArrayRef 類別非常相關(但專門用於字元陣列)。由於 StringRef 帶有長度,因此它可以安全地處理其中嵌入空字元的字串,取得長度不需要 strlen 呼叫,並且它甚至具有非常方便的 API,用於切片和切割它表示的字元範圍。
StringRef 非常適合傳遞已知為活動狀態的簡單字串,無論它們是 C 字串文字、std::string、C 陣列還是 SmallVector。這些情況中的每一種都具有到 StringRef 的有效率隱含轉換,這不會導致執行動態 strlen。
StringRef 有一些主要的限制,這些限制使得更強大的字串容器很有用:
您無法直接將 StringRef 轉換為 ‘const char*’,因為沒有辦法新增尾隨空字元(不像各種更強大類別上的 .c_str() 方法)。
StringRef 不擁有或保持底層字串位元組的存活。因此,它很容易導致懸空指標,並且在大多數情況下不適合嵌入到資料結構中(而是使用 std::string 或類似的東西)。
出於相同的原因,如果方法「計算」結果字串,則 StringRef 不能用作方法的傳回值。而是使用 std::string。
StringRef 不允許您修改指向的字串位元組,也不允許您從範圍中插入或移除位元組。對於這樣的編輯操作,它與 Twine 類別互操作。
由於其優點和限制,函數採用 StringRef 以及物件上的方法傳回指向它擁有的某些字串的 StringRef 非常常見。
llvm/ADT/Twine.h¶
Twine 類別用作 API 的中介資料類型,這些 API 想要採用可以使用一系列串聯內聯建構的字串。Twine 的工作原理是在堆疊上形成 Twine 資料類型(一個簡單的值物件)的遞迴實例作為臨時物件,將它們連結在一起形成一個樹狀結構,然後在 Twine 被消耗時將其線性化。Twine 僅在作為函數的引數時使用才是安全的,並且應始終是 const 參考,例如:
void foo(const Twine &T);
...
StringRef X = ...
unsigned i = ...
foo(X + "." + Twine(i));
此範例透過將值串聯在一起來形成類似 “blarg.42” 的字串,並且不形成包含 “blarg” 或 “blarg.” 的中間字串。
由於 Twine 是使用堆疊上的臨時物件建構的,並且由於這些實例在目前語句結束時被銷毀,因此它本質上是一個危險的 API。例如,這個簡單的變體包含未定義的行為,並且可能會崩潰:
void foo(const Twine &T);
...
StringRef X = ...
unsigned i = ...
const Twine &Tmp = X + "." + Twine(i);
foo(Tmp);
... 因為臨時物件在呼叫之前被銷毀。也就是說,Twine 比中間 std::string 臨時物件有效率得多,而且它們與 StringRef 非常搭配。只是要注意它們的限制。
llvm/ADT/SmallString.h¶
SmallString 是 SmallVector 的子類別,它新增了一些便利的 API,例如 +=,它採用 StringRef。當預先分配的空間足以容納其資料時,SmallString 避免分配記憶體,並且在需要時回呼到一般堆積分配。由於它擁有其資料,因此使用起來非常安全,並且支援字串的完整修改。
與 SmallVector 類似,SmallString 的主要缺點是它們的 sizeof。雖然它們針對小型字串進行了最佳化,但它們本身並不是特別小。這表示它們非常適合堆疊上的臨時暫存緩衝區,但不應通常放入堆積中:很少看到 SmallString 作為頻繁分配的堆積資料結構的成員或按值傳回。
std::string¶
標準 C++ std::string 類別是一個非常通用的類別,它(像 SmallString 一樣)擁有其底層資料。sizeof(std::string) 非常合理,因此可以嵌入到堆積資料結構中並按值傳回。另一方面,std::string 對於內聯編輯(例如,將一堆東西串聯在一起)非常沒有效率,並且由於它是由標準函式庫提供的,因此其效能特性很大程度上取決於主機標準函式庫(例如,libc++ 和 MSVC 提供了高度最佳化的字串類別,GCC 包含一個非常慢的實作)。
std::string 的主要缺點是幾乎每個使其變大的操作都會分配記憶體,這很慢。因此,最好使用 SmallVector 或 Twine 作為暫存緩衝區,然後使用 std::string 來持久化結果。
類似集合的容器(std::set、SmallSet、SetVector 等)¶
當您需要將多個數值標準化為單一表示法時,類似集合的容器非常有用。有多種不同的方法可以做到這一點,並提供各種權衡考量。
排序過的「vector」¶
如果您打算插入大量元素,然後進行大量查詢,一個很好的方法是使用 std::vector(或其他循序容器)搭配 std::sort+std::unique 來移除重複項。如果您的使用模式具有這兩個截然不同的階段(插入然後查詢),並且可以與循序容器的良好選擇結合使用,則此方法非常有效。
這種組合提供了幾個優良特性:結果資料在記憶體中是連續的(有利於快取局部性),配置次數少,易於定址(最終 vector 中的迭代器只是索引或指標),並且可以使用標準二元搜尋有效地查詢(例如 std::lower_bound
;如果您想要比較相等的元素的整個範圍,請使用 std::equal_range
)。
llvm/ADT/SmallSet.h¶
如果您有一個類似集合的資料結構,它通常很小,並且其元素也相當小,那麼 SmallSet<Type, N>
是一個不錯的選擇。此集合在原地配置了 N 個元素的空間(因此,如果集合動態地小於 N,則不需要 malloc 流量),並使用簡單的線性搜尋來存取它們。當集合成長超出 N 個元素時,它會分配更昂貴的表示法,以保證高效的存取(對於大多數類型,它會回退到 std::set,但對於指標,它使用更好的 SmallPtrSet)。
這個類別的魔力在於它可以極其高效地處理小型集合,但也能優雅地處理極大型集合而不會損失效率。
llvm/ADT/SmallPtrSet.h¶
SmallPtrSet
具有 SmallSet
的所有優點(並且指標的 SmallSet
是以 SmallPtrSet
透明地實作的)。如果執行超過 N 次插入,則會分配一個單一的二次探測雜湊表,並根據需要成長,從而提供極其高效的存取(恆定時間的插入/刪除/查詢,且恆定因子低),並且在 malloc 流量方面非常節省。
請注意,與 std::set 不同,每當發生插入或刪除時,SmallPtrSet
的迭代器都會失效。remove_if
方法可用於在迭代集合時移除元素。
此外,迭代器訪問的值並非以排序順序訪問。
llvm/ADT/StringSet.h¶
StringSet
是 StringMap<char> 的一個輕薄封裝器,它允許高效地儲存和檢索唯一字串。
在功能上類似於 SmallSet<StringRef>
,StringSet
也支援迭代。(迭代器會解引用為 StringMapEntry<char>
,因此您需要呼叫 i->getKey()
才能存取 StringSet 的項目。)另一方面,StringSet
不支援範圍插入和複製建構,而 SmallSet 和 SmallPtrSet 則支援。
llvm/ADT/DenseSet.h¶
DenseSet 是一個簡單的二次探測雜湊表。它擅長支援小數值:它使用單一配置來容納目前插入集合中的所有配對。DenseSet 是一種對非簡單指標的小數值進行唯一化的絕佳方法(指標請使用 SmallPtrSet)。請注意,DenseSet 對於數值類型的要求與 DenseMap 相同。
llvm/ADT/SparseSet.h¶
SparseSet 保留少量由中等大小的無號鍵識別的物件。它使用大量記憶體,但提供的操作幾乎與 vector 一樣快。典型的鍵是實體暫存器、虛擬暫存器或編號的基本區塊。
SparseSet 適用於需要非常快速的清除/尋找/插入/刪除以及快速迭代小型集合的演算法。它不適用於建構複合資料結構。
llvm/ADT/SparseMultiSet.h¶
SparseMultiSet 為 SparseSet 新增了多重集合行為,同時保留了 SparseSet 的理想屬性。與 SparseSet 一樣,它通常使用大量記憶體,但提供的操作幾乎與 vector 一樣快。典型的鍵是實體暫存器、虛擬暫存器或編號的基本區塊。
SparseMultiSet 適用於需要非常快速地清除/尋找/插入/刪除整個集合,以及迭代共用一個鍵的元素集合的演算法。它通常是比使用複合資料結構(例如,vector-of-vectors、map-of-vectors)更有效率的選擇。它不適用於建構複合資料結構。
llvm/ADT/FoldingSet.h¶
FoldingSet 是一個聚合類別,非常擅長對建立成本高昂或多型的物件進行唯一化。它是鏈式雜湊表與侵入式連結(唯一化物件需要繼承自 FoldingSetNode)的組合,它使用 SmallVector 作為其 ID 處理的一部分。
考慮這樣一個案例,您想要為複雜物件實作一個「getOrCreateFoo」方法(例如,程式碼產生器中的節點)。用戶端有一個關於它想要產生**什麼**的描述(它知道運算碼和所有運算元),但我們不想 'new' 一個節點,然後嘗試將其插入集合中,結果卻發現它已經存在,在這種情況下,我們必須刪除它並返回已經存在的節點。
為了支援這種風格的用戶端,FoldingSet 使用 FoldingSetNodeID(它封裝了 SmallVector)執行查詢,FoldingSetNodeID 可用於描述我們想要查詢的元素。查詢會傳回與 ID 相符的元素,或者傳回一個不透明的 ID,指示應該在何處進行插入。ID 的建構通常不需要堆積流量。
由於 FoldingSet 使用侵入式連結,因此它可以支援集合中的多型物件(例如,您可以將 SDNode 實例與 LoadSDNode 混合使用)。由於元素是個別配置的,因此指向元素的指標是穩定的:插入或移除元素不會使指向其他元素的任何指標失效。
<set>¶
std::set
是一個合理的通用集合類別,它在許多方面表現尚可,但在任何方面都不出色。std::set 為每個插入的元素分配記憶體(因此它非常消耗 malloc),並且通常在集合中為每個元素儲存三個指標(因此增加了大量的每個元素空間開銷)。它提供保證的 log(n) 效能,從複雜度的角度來看,這並不是特別快(尤其是當集合的元素比較成本很高時,例如字串),並且在查找、插入和移除方面具有極高的常數因子。
std::set 的優點是其迭代器是穩定的(從集合中刪除或插入元素不會影響迭代器或指向其他元素的指標),並且保證對集合的迭代是以排序順序進行的。如果集合中的元素很大,那麼指標和 malloc 流量的相對開銷並不是什麼大問題,但如果集合的元素很小,那麼 std::set 幾乎永遠不是一個好的選擇。
llvm/ADT/SetVector.h¶
LLVM 的 SetVector<Type>
是一個配接器類別,它將您選擇的類似集合的容器與循序容器結合在一起。它提供的最重要的屬性是高效插入且具有唯一化功能(重複元素會被忽略)以及迭代支援。它透過將元素插入到類似集合的容器和循序容器中來實作此功能,使用類似集合的容器進行唯一化,使用循序容器進行迭代。
SetVector 與其他集合之間的區別在於,迭代順序保證與插入 SetVector 的順序相符。對於指標集合之類的東西來說,這個屬性非常重要。由於指標值是不確定的(例如,在不同機器上執行的程式之間會有所不同),因此迭代集合中的指標將不會以明確定義的順序進行。
SetVector 的缺點是它需要的空間是普通集合的兩倍,並且具有它使用的類似集合的容器和循序容器的常數因子之和。**僅**在您需要以確定性順序迭代元素時才使用它。SetVector 刪除元素的成本也很高(線性時間),除非您使用其「pop_back」方法,否則會更快。
SetVector
是一個配接器類別,預設使用 std::vector
和大小為 16 的 SmallSet
作為底層容器,因此它非常昂貴。但是,"llvm/ADT/SetVector.h"
也提供了一個 SmallSetVector
類別,它預設使用 SmallVector
和指定大小的 SmallSet
。如果您使用它,並且您的集合在動態上小於 N
,您將節省大量的堆積流量。
llvm/ADT/UniqueVector.h¶
UniqueVector 類似於 SetVector,但它為每個插入集合的元素保留一個唯一 ID。它在內部包含一個 map 和一個 vector,並且為每個插入集合的值分配一個唯一 ID。
UniqueVector 非常昂貴:它的成本是維護 map 和 vector 的成本之和,它具有高複雜度、高常數因子,並產生大量的 malloc 流量。應該避免使用它。
llvm/ADT/ImmutableSet.h¶
ImmutableSet 是一個基於 AVL 樹的不可變(函數式)集合實作。新增或移除元素是透過 Factory 物件完成的,並產生一個新的 ImmutableSet 物件。如果具有給定內容的 ImmutableSet 已經存在,則會返回現有的 ImmutableSet;相等性是使用 FoldingSetNodeID 比較的。新增或移除操作的時間和空間複雜度與原始集合的大小成對數關係。
沒有用於返回集合元素的方法,您只能檢查成員資格。
其他類似集合的容器選項¶
STL 提供了其他幾個選項,例如 std::multiset 和 std::unordered_set。我們從不使用像 unordered_set 這樣的容器,因為它們通常非常昂貴(每次插入都需要 malloc)。
如果您不關心消除重複項,std::multiset 會很有用,但它具有 std::set 的所有缺點。排序後的 vector(您不刪除重複條目的情況)或其他一些方法幾乎總是更好。
類似 Map 的容器(std::map、DenseMap 等)¶
當您想要將資料與鍵關聯時,類似 Map 的容器非常有用。與往常一樣,有很多不同的方法可以做到這一點。 :)
排序過的「vector」¶
如果您的使用模式遵循嚴格的插入然後查詢方法,您可以輕易地使用與用於類似集合容器的排序 vector相同的方法。唯一的區別是您的查詢函式(使用 std::lower_bound 來獲得高效的 log(n) 查找)應該只比較鍵,而不是鍵和值都比較。這產生了與用於集合的排序 vector 相同的優點。
llvm/ADT/StringMap.h¶
字串通常用作 map 中的鍵,並且它們很難有效地支援:它們的長度可變、在長字串時雜湊和比較效率低下、複製成本高等等。StringMap 是一個專門設計用於應對這些問題的容器。它支援將任意位元組範圍對應到任意其他物件。
StringMap 實作使用二次探測雜湊表,其中 bucket 儲存指向堆積分配條目的指標(以及其他一些東西)。map 中的條目必須是堆積分配的,因為字串的長度是可變的。字串資料(鍵)和元素物件(值)儲存在同一個配置中,字串資料緊接在元素物件之後。此容器保證「(char*)(&Value+1)
」指向值的鍵字串。
StringMap 速度非常快的原因有幾個:二次探測對於查找非常快取效率高,在查找元素時不會重新計算 bucket 中字串的雜湊值,即使發生雜湊衝突,StringMap 也很少需要接觸不相關物件的記憶體,雜湊表成長不會重新計算表中已有的字串的雜湊值,並且 map 中的每個配對都儲存在單一配置中(字串資料與配對的值儲存在同一個配置中)。
StringMap 也提供採用位元組範圍的查詢方法,因此只有在將值插入到表中時才會複製字串。
但是,StringMap 迭代順序不保證是確定性的,因此任何需要確定性順序的用途都應該改用 std::map。
llvm/ADT/IndexedMap.h¶
IndexedMap 是一個專門的容器,用於將小的密集整數(或可以對應到小的密集整數的值)對應到其他類型。它在內部實作為一個 vector,具有一個將鍵對應到密集整數範圍的對應函式。
這對於像 LLVM 程式碼產生器中的虛擬暫存器這樣的案例很有用:它們具有一個密集對應,該對應由編譯時常數(第一個虛擬暫存器 ID)偏移。
llvm/ADT/DenseMap.h¶
DenseMap 是一個簡單的二次探測雜湊表。它擅長支援小鍵和值:它使用單一配置來容納目前插入 map 中的所有配對。DenseMap 是一種將指標對應到指標,或將其他小型類型彼此對應的絕佳方法。
但是,您應該注意 DenseMap 的幾個方面。與 map 不同,每當發生插入時,DenseMap 中的迭代器都會失效。此外,由於 DenseMap 為大量的鍵/值配對分配空間(預設從 64 開始),如果您的鍵或值很大,它將浪費大量空間。最後,如果您要使用的鍵類型尚未支援,您必須為該鍵類型實作 DenseMapInfo 的部分特化。這是必需的,以便告知 DenseMap 關於它在內部需要的兩個特殊標記值(永遠不能插入到 map 中)。
DenseMap 的 find_as() 方法支援使用替代鍵類型進行查找操作。這在正常鍵類型建構成本高昂,但與之比較成本低廉的情況下很有用。DenseMapInfo 負責為每個使用的替代鍵類型定義適當的比較和雜湊方法。
DenseMap.h 也包含一個 SmallDenseMap 變體,它類似於 SmallVector,在範本參數 N 中的元素數量超過之前,不會執行堆積分配。
llvm/IR/ValueMap.h¶
ValueMap 是 DenseMap 的一個封裝器,它將 Value*
(或子類別)對應到另一種類型。當 Value 被刪除或 RAUW'ed 時,ValueMap 將自行更新,以便新版本的鍵對應到相同的值,就像鍵是 WeakVH 一樣。您可以透過將 Config
參數傳遞給 ValueMap 範本,來配置這一切如何發生,以及在這兩個事件中還會發生什麼。
llvm/ADT/IntervalMap.h¶
IntervalMap 是一個用於小鍵和小值的緊湊 map。它對應鍵區間而不是單一鍵,並且它會自動合併相鄰區間。當 map 僅包含少量區間時,它們會儲存在 map 物件本身中以避免配置。
IntervalMap 迭代器相當大,因此不應將它們作為 STL 迭代器傳遞。重量級迭代器允許更小的資料結構。
llvm/ADT/IntervalTree.h¶
llvm::IntervalTree
是一個輕量級樹狀資料結構,用於保存區間。它允許查找與任何給定點重疊的所有區間。目前,它不支援任何刪除或重新平衡操作。
IntervalTree 設計為設定一次,然後在不進行任何進一步新增的情況下進行查詢。
<map>¶
std::map 具有與 std::set 相似的特性:它為每個插入 map 的配對使用單一配置,它提供 log(n) 查找,但常數因子極大,每個 map 中的配對會強加 3 個指標的空間代價等等。
當您的鍵或值非常大,如果您需要以排序順序迭代集合,或者如果您需要 map 中穩定的迭代器(即,如果發生另一個元素的插入或刪除,它們不會失效),std::map 最有用。
llvm/ADT/MapVector.h¶
MapVector<KeyT,ValueT>
提供了 DenseMap 介面的子集。主要區別在於迭代順序保證是插入順序,使其成為指標 map 上非確定性迭代的簡單(但有點昂貴)解決方案。
它的實作方式是從鍵對應到鍵值配對 vector 中的索引。這提供了快速查找和迭代,但有兩個主要缺點:鍵儲存了兩次,並且移除元素需要線性時間。如果必須移除元素,最好使用 remove_if()
批量移除它們。
llvm/ADT/IntEqClasses.h¶
IntEqClasses 提供了小整數等價類別的緊湊表示法。最初,範圍 0..n-1 中的每個整數都有自己的等價類別。類別可以透過將兩個類別代表傳遞給 join(a, b) 方法來加入。當 findLeader() 返回相同的代表時,兩個整數屬於同一個類別。
一旦形成所有等價類別,就可以壓縮 map,以便每個整數 0..n-1 對應到範圍 0..m-1 中的一個等價類別編號,其中 m 是等價類別的總數。map 必須先解壓縮才能再次編輯。
llvm/ADT/ImmutableMap.h¶
ImmutableMap 是一個基於 AVL 樹的不可變(函數式)map 實作。新增或移除元素是透過 Factory 物件完成的,並產生一個新的 ImmutableMap 物件。如果具有給定鍵集合的 ImmutableMap 已經存在,則會返回現有的 ImmutableMap;相等性是使用 FoldingSetNodeID 比較的。新增或移除操作的時間和空間複雜度與原始 map 的大小成對數關係。
其他類似 Map 的容器選項¶
STL 提供了其他幾個選項,例如 std::multimap 和 std::unordered_map。我們從不使用像 unordered_map 這樣的容器,因為它們通常非常昂貴(每次插入都需要 malloc)。
如果您想要將一個鍵對應到多個值,std::multimap 會很有用,但它具有 std::map 的所有缺點。排序後的 vector 或其他一些方法幾乎總是更好。
位元儲存容器¶
有多個位元儲存容器,選擇何時使用每個容器相對簡單明瞭。
另一個選項是 std::vector<bool>
:我們不鼓勵使用它,原因有兩個:1) 許多常見編譯器(例如,常用版本的 GCC)中的實作效率極低,以及 2) C++ 標準委員會很可能會棄用此容器和/或以某種方式顯著更改它。在任何情況下,請不要使用它。
BitVector¶
BitVector 容器提供了一個動態大小的位元集合,用於操作。它支援個別位元設定/測試,以及集合操作。集合操作需要 O(位元向量大小) 的時間,但操作是以一次一個字組而不是一次一個位元的方式執行的。這使得 BitVector 與其他容器相比,在集合操作方面非常快速。當您預期設定的位元數量很高時(即,密集集合),請使用 BitVector。
SmallBitVector¶
SmallBitVector 容器提供與 BitVector 相同的介面,但針對只需要少量位元(少於 25 個左右)的情況進行了最佳化。它也透明地支援更大的位元計數,但效率略低於普通的 BitVector,因此只有在較大的計數很少見時才應使用 SmallBitVector。
目前,SmallBitVector 不支援集合操作(and、or、xor),並且其 operator[] 不提供可指派的左值。
SparseBitVector¶
SparseBitVector 容器非常像 BitVector,但有一個主要區別:只儲存設定的位元。當集合稀疏時,這使得 SparseBitVector 比 BitVector 更節省空間,並且使集合操作的複雜度為 O(設定位元數量) 而不是 O(宇宙大小)。SparseBitVector 的缺點是隨機位元的設定和測試是 O(N) 的,並且在大型 SparseBitVector 上,這可能比 BitVector 慢。在我們的實作中,以排序順序(向前或向後)設定或測試位元是 O(1) 最壞情況。測試和設定距離目前位元 128 位元以內的位元也是 O(1)。作為一般性陳述,在 SparseBitVector 中測試/設定位元是 O(距離上次設定位元的距離)。
CoalescingBitVector¶
CoalescingBitVector 容器在原理上與 SparseBitVector 相似,但經過最佳化,可以緊湊地表示大型連續的設定位元範圍。它透過將連續的設定位元範圍合併為區間來做到這一點。在 CoalescingBitVector 中搜尋位元的複雜度為 O(log(連續範圍之間的間隙))。
當設定位元範圍之間的間隙很大時,CoalescingBitVector 是比 BitVector 更好的選擇。當 find() 操作必須具有快速、可預測的效能時,它是比 SparseBitVector 更好的選擇。但是,對於表示具有大量非常短範圍的集合來說,它不是一個好的選擇。例如,集合 {2*x : x in [0, n)} 將是一個病態輸入。
實用的工具函式¶
LLVM 實作了許多在整個程式碼庫中使用的通用工具函式。您可以在 STLExtras.h
(doxygen) 中找到最常見的函式。其中一些封裝了著名的 C++ 標準函式庫函式,而另一些則是 LLVM 獨有的。
迭代範圍¶
有時您可能想要一次迭代多個範圍,或知道索引的索引。LLVM 提供了自訂的工具函式,以簡化此操作,而無需手動管理所有迭代器和/或索引
zip
* 函式¶
zip
* 函式允許同時迭代來自兩個或多個範圍的元素。例如
SmallVector<size_t> Counts = ...;
char Letters[26] = ...;
for (auto [Letter, Count] : zip_equal(Letters, Counts))
errs() << Letter << ": " << Count << "\n";
請注意,這些元素是透過「參考包裝器」代理類型(參考的元組)提供的,這與結構化綁定宣告結合,使得 Letter
和 Count
成為範圍元素的參考。對這些參考的任何修改都會影響 Letters
或 Counts
的元素。
zip
* 函數支援臨時範圍,例如:
for (auto [Letter, Count] : zip(SmallVector<char>{'a', 'b', 'c'}, Counts))
errs() << Letter << ": " << Count << "\n";
zip
系列函數之間的差異在於,當提供的範圍長度不同時,它們的行為方式有所不同
zip_equal
– 要求所有輸入範圍都具有相同的長度。zip
– 當到達最短範圍的末尾時,迭代停止。zip_first
– 要求第一個範圍是最短的範圍。zip_longest
– 迭代持續到到達最長範圍的末尾。較短範圍中不存在的元素會被替換為std::nullopt
。
長度要求會使用 assert
進行檢查。
根據經驗法則,當您預期所有範圍都具有相同的長度時,請優先使用 zip_equal
,並且僅在情況並非如此時才考慮使用其他 zip
函數。這是因為 zip_equal
清楚地傳達了這種相同長度的假設,並且具有最佳的(發布模式)運行時效能。
enumerate
¶
enumerate
函數允許在一個或多個範圍上迭代,同時追蹤當前迴圈迭代的索引。例如:
for (auto [Idx, BB, Value] : enumerate(Phi->blocks(),
Phi->incoming_values()))
errs() << "#" << Idx << " " << BB->getName() << ": " << *Value << "\n";
當前元素索引作為第一個結構化綁定元素提供。或者,可以使用 index()
和 value()
成員函數獲取索引和元素值。
char Letters[26] = ...;
for (auto En : enumerate(Letters))
errs() << "#" << En.index() << " " << En.value() << "\n";
請注意,enumerate
具有 zip_equal
的語義,並透過「參考包裝器」代理提供元素,這使得它們在透過結構化綁定或 value()
成員函數訪問時可以被修改。當傳遞兩個或多個範圍時,enumerate
要求它們具有相等的長度(使用 assert
檢查)。
除錯 (Debugging)¶
為了一些核心 LLVM 函式庫,提供了一些 GDB 漂亮列印器 (pretty printers)。要使用它們,請執行以下操作(或將其添加到您的 ~/.gdbinit
中):
source /path/to/llvm/src/utils/gdb-scripts/prettyprinters.py
啟用 print pretty 選項也可能很方便,以避免資料結構被列印為一大塊文字。
常用操作的實用提示 (Helpful Hints for Common Operations)¶
本節介紹如何執行 LLVM 程式碼的一些非常簡單的轉換。目的是提供常用慣用語的範例,展示 LLVM 轉換的實務方面。
因為這是「操作指南」章節,您也應該閱讀您將使用的主要類別。核心 LLVM 類別階層參考 (Core LLVM Class Hierarchy Reference) 包含您應該了解的主要類別的詳細資訊和描述。
基本檢查和遍歷常式 (Basic Inspection and Traversal Routines)¶
LLVM 編譯器基礎架構有許多不同的資料結構可以遍歷。遵循 C++ 標準模板函式庫的範例,用於遍歷這些各種資料結構的技術基本上都是相同的。對於可列舉的值序列,XXXbegin()
函數(或方法)傳回指向序列開始的迭代器,XXXend()
函數傳回指向序列中最後一個有效元素之後位置的迭代器,並且在兩個操作之間存在一些通用的 XXXiterator
資料類型。
由於迭代模式在程式表示的許多不同方面都很常見,因此標準模板函式庫演算法可以在它們上使用,並且更容易記住如何迭代。首先,我們展示一些需要遍歷的資料結構的常見範例。其他資料結構的遍歷方式非常相似。
迭代 Function
中的 BasicBlock
(Iterating over the BasicBlock
in a Function
)¶
很常見的情況是,您有一個 Function
實例,您想要以某種方式轉換它;特別是,您想要操作它的 BasicBlock
。為了方便起見,您需要迭代構成 Function
的所有 BasicBlock
。以下是一個範例,它列印 BasicBlock
的名稱以及它包含的 Instruction
數量:
Function &Func = ...
for (BasicBlock &BB : Func)
// Print out the name of the basic block if it has one, and then the
// number of instructions that it contains
errs() << "Basic block (name=" << BB.getName() << ") has "
<< BB.size() << " instructions.\n";
迭代 BasicBlock
中的 Instruction
(Iterating over the Instruction
in a BasicBlock
)¶
就像處理 Function
中的 BasicBlock
一樣,可以很容易地迭代構成 BasicBlock
的個別指令。以下程式碼片段列印出 BasicBlock
中的每個指令:
BasicBlock& BB = ...
for (Instruction &I : BB)
// The next statement works since operator<<(ostream&,...)
// is overloaded for Instruction&
errs() << I << "\n";
然而,這並不是列印 BasicBlock
內容的最佳方式!由於 ostream 運算符針對幾乎所有您關心的內容都進行了重載,因此您可以直接對基本區塊本身調用列印常式:errs() << BB << "\n";
。
迭代 Function
中的 Instruction
(Iterating over the Instruction
in a Function
)¶
如果您發現您經常迭代 Function
的 BasicBlock
,然後迭代該 BasicBlock
的 Instruction
,則應該改用 InstIterator
。您需要包含 llvm/IR/InstIterator.h
(doxygen),然後在您的程式碼中顯式實例化 InstIterator
。以下是一個小範例,展示如何將函數中的所有指令轉儲到標準錯誤流:
#include "llvm/IR/InstIterator.h"
// F is a pointer to a Function instance
for (inst_iterator I = inst_begin(F), E = inst_end(F); I != E; ++I)
errs() << *I << "\n";
很簡單,不是嗎?您也可以使用 InstIterator
來使用其初始內容填充工作列表。例如,如果您想要初始化一個工作列表以包含 Function
F 中的所有指令,您只需執行類似以下的操作:
std::set<Instruction*> worklist;
// or better yet, SmallPtrSet<Instruction*, 64> worklist;
for (inst_iterator I = inst_begin(F), E = inst_end(F); I != E; ++I)
worklist.insert(&*I);
現在,STL 集合 worklist
將包含 Function
F 指向的所有指令。
將迭代器轉換為類別指標(反之亦然)(Turning an iterator into a class pointer (and vice-versa))¶
有時,當您手邊只有迭代器時,獲取對類別實例的參考(或指標)會很有用。好吧,從迭代器中提取參考或指標非常簡單。假設 i
是 BasicBlock::iterator
,而 j
是 BasicBlock::const_iterator
:
Instruction& inst = *i; // Grab reference to instruction reference
Instruction* pinst = &*i; // Grab pointer to instruction reference
const Instruction& inst = *j;
也可以將類別指標轉換為對應的迭代器,這是一個常數時間操作(非常有效率)。以下程式碼片段說明了 LLVM 迭代器提供的轉換建構子的使用。透過使用這些建構子,您可以顯式獲取某個事物的迭代器,而無需實際透過迭代某些結構來獲取它:
void printNextInstruction(Instruction* inst) {
BasicBlock::iterator it(inst);
++it; // After this line, it refers to the instruction after *inst
if (it != inst->getParent()->end()) errs() << *it << "\n";
}
尋找調用點:一個稍微複雜的範例 (Finding call sites: a slightly more complex example)¶
假設您正在編寫一個 FunctionPass,並且想要計算整個模組(即跨越每個 Function
)中,某個特定函數(即某個 Function *
)已在作用域中的所有位置。正如您稍後將了解到的,您可能想要使用 InstVisitor
以更直接的方式完成此操作,但此範例將讓我們探索如果您沒有 InstVisitor
在身邊,您將如何執行此操作。在虛擬碼中,這就是我們想要做的事情:
initialize callCounter to zero
for each Function f in the Module
for each BasicBlock b in f
for each Instruction i in b
if (i a Call and calls the given function)
increment callCounter
實際程式碼如下(請記住,由於我們正在編寫 FunctionPass
,因此我們的 FunctionPass
派生類別只需覆寫 runOnFunction
方法):
Function* targetFunc = ...;
class OurFunctionPass : public FunctionPass {
public:
OurFunctionPass(): callCounter(0) { }
virtual runOnFunction(Function& F) {
for (BasicBlock &B : F) {
for (Instruction &I: B) {
if (auto *CB = dyn_cast<CallBase>(&I)) {
// We know we've encountered some kind of call instruction (call,
// invoke, or callbr), so we need to determine if it's a call to
// the function pointed to by m_func or not.
if (CB->getCalledFunction() == targetFunc)
++callCounter;
}
}
}
}
private:
unsigned callCounter;
};
迭代定義-使用和使用-定義鏈 (Iterating over def-use & use-def chains)¶
通常,我們可能有一個 Value
類別 (doxygen) 的實例,並且我們想要確定哪些 User
使用了該 Value
。特定 Value
的所有 User
的列表稱為 *定義-使用* 鏈。例如,假設我們有一個名為 F
的 Function*
指向特定的函數 foo
。尋找所有 *使用* foo
的指令就像迭代 F
的 *定義-使用* 鏈一樣簡單:
Function *F = ...;
for (User *U : F->users()) {
if (Instruction *Inst = dyn_cast<Instruction>(U)) {
errs() << "F is used in instruction:\n";
errs() << *Inst << "\n";
}
或者,常見的情況是擁有 User
類別 (doxygen) 的實例,並且需要知道它使用了哪些 Value
。 User
使用的所有 Value
的列表稱為 *使用-定義* 鏈。 Instruction
類別的實例是常見的 User
,因此我們可能想要迭代特定指令使用的所有值(即,特定 Instruction
的運算元):
Instruction *pi = ...;
for (Use &U : pi->operands()) {
Value *v = U.get();
// ...
}
將物件宣告為 const
是強制執行無變更演算法(例如分析等)的重要工具。為此,上述迭代器以常數形式出現,如 Value::const_use_iterator
和 Value::const_op_iterator
。當在 const Value*
或 const User*
上調用 use/op_begin()
時,它們會自動出現。在取消引用時,它們會傳回 const Use*
。否則,上述模式保持不變。
迭代區塊的前導區塊和後繼區塊 (Iterating over predecessors & successors of blocks)¶
使用 "llvm/IR/CFG.h"
中定義的常式,可以很容易地迭代區塊的前導區塊和後繼區塊。只需使用像這樣的程式碼來迭代 BB 的所有前導區塊:
#include "llvm/IR/CFG.h"
BasicBlock *BB = ...;
for (BasicBlock *Pred : predecessors(BB)) {
// ...
}
類似地,要迭代後繼區塊,請使用 successors
。
進行簡單的變更 (Making simple changes)¶
LLVM 基礎架構中存在一些值得了解的原始轉換操作。在執行轉換時,操作基本區塊的內容是很常見的。本節介紹了一些常用的方法來執行此操作,並提供範例程式碼。
建立和插入新的 Instruction
(Creating and inserting new Instruction
s)¶
實例化指令 (Instantiating Instructions)
建立 Instruction
很簡單:只需調用要實例化的指令種類的建構子,並提供必要的參數。例如,AllocaInst
僅 *需要* 一個(指向常數的指標)Type
。因此:
auto *ai = new AllocaInst(Type::Int32Ty);
將建立一個 AllocaInst
實例,它表示在執行時在當前堆疊框架中分配一個整數。每個 Instruction
子類別都可能具有不同的預設參數,這些參數會更改指令的語義,因此請參閱您感興趣實例化的 Instruction 子類別的 doxygen 文件。
命名值 (Naming values)
當您可以命名指令的值時,這非常有用,因為這有助於除錯您的轉換。如果您最終查看產生的 LLVM 機器碼,您肯定希望將邏輯名稱與指令結果關聯起來!透過為 Instruction
建構子的 Name
(預設)參數提供值,您可以將邏輯名稱與指令在執行時的執行結果關聯起來。例如,假設我正在編寫一個轉換,該轉換在堆疊上動態分配整數的空間,並且該整數將被某些其他程式碼用作某種索引。為了完成此操作,我在某個 Function
的第一個 BasicBlock
的第一個點放置一個 AllocaInst
,並且我打算在同一個 Function
中使用它。我可能會這樣做:
auto *pa = new AllocaInst(Type::Int32Ty, 0, "indexLoc");
其中 indexLoc
現在是指令執行值的邏輯名稱,它是指向執行時堆疊上整數的指標。
插入指令 (Inserting instructions)
基本上有三種方法將 Instruction
插入到構成 BasicBlock
的現有指令序列中:
插入到
BasicBlock
的指令列表中 (Insertion into the instruction list of theBasicBlock
)給定一個
BasicBlock* pb
、該BasicBlock
中的Instruction* pi
,以及我們想要在*pi
之前插入的新建立的指令,我們執行以下操作:BasicBlock *pb = ...; Instruction *pi = ...; auto *newInst = new Instruction(...); newInst->insertBefore(pi); // Inserts newInst before pi
附加到
BasicBlock
的末尾非常常見,以至於Instruction
類別和Instruction
派生類別提供了建構子,這些建構子接受要附加到的BasicBlock
的指標。例如,看起來像這樣的程式碼:BasicBlock *pb = ...; auto *newInst = new Instruction(...); newInst->insertInto(pb, pb->end()); // Appends newInst to pb
變成:
BasicBlock *pb = ...; auto *newInst = new Instruction(..., pb);
這更加簡潔,尤其是在您建立長指令流的情況下。
使用
IRBuilder
的實例插入 (Insertion using an instance ofIRBuilder
)使用先前的方法插入多個
Instruction
可能相當費力。IRBuilder
是一個方便類別,可用於將多個指令添加到BasicBlock
的末尾或特定Instruction
之前。它還支援常數摺疊和重新命名命名暫存器(請參閱IRBuilder
的模板參數)。下面的範例示範了
IRBuilder
的一個非常簡單的用法,其中在指令pi
之前插入了三個指令。前兩個指令是 Call 指令,第三個指令將兩個調用的傳回值相乘。Instruction *pi = ...; IRBuilder<> Builder(pi); CallInst* callOne = Builder.CreateCall(...); CallInst* callTwo = Builder.CreateCall(...); Value* result = Builder.CreateMul(callOne, callTwo);
下面的範例與上面的範例類似,不同之處在於建立的
IRBuilder
在BasicBlock
pb
的末尾插入指令。BasicBlock *pb = ...; IRBuilder<> Builder(pb); CallInst* callOne = Builder.CreateCall(...); CallInst* callTwo = Builder.CreateCall(...); Value* result = Builder.CreateMul(callOne, callTwo);
有關
IRBuilder
的實際使用,請參閱 Kaleidoscope 教學 (Kaleidoscope Tutorial)。
刪除指令 (Deleting Instructions)¶
從構成 BasicBlock 的現有指令序列中刪除指令非常簡單:只需調用指令的 eraseFromParent()
方法。例如:
Instruction *I = .. ;
I->eraseFromParent();
這會將指令從其包含的基本區塊中取消連結並刪除它。如果您只想從其包含的基本區塊中取消連結指令,但不刪除它,則可以使用 removeFromParent()
方法。
將指令替換為另一個值 (Replacing an Instruction with another Value)¶
替換個別指令 (Replacing individual instructions)¶
包含「llvm/Transforms/Utils/BasicBlockUtils.h」允許使用兩個非常有用的替換函數:ReplaceInstWithValue
和 ReplaceInstWithInst
。
刪除指令 (Deleting Instructions)¶
ReplaceInstWithValue
此函數將給定指令的所有用法替換為一個值,然後移除原始指令。以下範例說明如何將特定
AllocaInst
的結果(為單個整數分配記憶體)替換為指向整數的空指標。AllocaInst* instToReplace = ...; BasicBlock::iterator ii(instToReplace); ReplaceInstWithValue(ii, Constant::getNullValue(PointerType::getUnqual(Type::Int32Ty)));
ReplaceInstWithInst
此函數將特定指令替換為另一個指令,將新指令插入到基本區塊中舊指令所在的位置,並將舊指令的任何用法替換為新指令。以下範例說明如何將一個
AllocaInst
替換為另一個。AllocaInst* instToReplace = ...; BasicBlock::iterator ii(instToReplace); ReplaceInstWithInst(instToReplace->getParent(), ii, new AllocaInst(Type::Int32Ty, 0, "ptrToReplacedInt"));
替換 User 和 Value 的多個用法 (Replacing multiple uses of Users and Values)¶
您可以使用 Value::replaceAllUsesWith
和 User::replaceUsesOfWith
一次更改多個用法。有關更多資訊,請參閱 Value 類別 和 User 類別 的 doxygen 文件。
刪除全域變數 (Deleting GlobalVariables)¶
從模組中刪除全域變數就像刪除指令一樣容易。首先,您必須有一個指向要刪除的全域變數的指標。您可以使用此指標從其父模組中刪除它。例如:
GlobalVariable *GV = .. ;
GV->eraseFromParent();
執行緒和 LLVM (Threads and LLVM)¶
本節介紹 LLVM API 與多執行緒的互動,包括客戶端應用程式方面,以及 JIT(在託管應用程式中)。
請注意,LLVM 對多執行緒的支援仍然相對較新。在 2.5 版之前,支援執行緒化的託管應用程式的執行,但不支援對 API 的執行緒化客戶端存取。雖然現在支援此用例,但客戶端 *必須* 遵守下面指定的準則,以確保在多執行緒模式下正常運作。
請注意,在類似 Unix 的平台上,LLVM 需要存在 GCC 的原子內建函數,才能支援執行緒化操作。如果您需要在沒有合適的現代系統編譯器的平台上使用支援多執行緒的 LLVM,請考慮在單執行緒模式下編譯 LLVM 和 LLVM-GCC,並使用產生的編譯器建置支援多執行緒的 LLVM 副本。
使用 llvm_shutdown()
結束執行 (Ending Execution with llvm_shutdown()
)¶
當您完成使用 LLVM API 時,您應該調用 llvm_shutdown()
以釋放用於內部結構的記憶體。
使用 ManagedStatic
進行延遲初始化 (Lazy Initialization with ManagedStatic
)¶
ManagedStatic
是 LLVM 中的一個實用類別,用於實作靜態資源(例如全域類型表)的靜態初始化。在單執行緒環境中,它實作了一個簡單的延遲初始化方案。但是,當 LLVM 編譯時支援多執行緒時,它會使用雙重檢查鎖定來實作執行緒安全的延遲初始化。
使用 LLVMContext
實現隔離 (Achieving Isolation with LLVMContext
)¶
LLVMContext
是 LLVM API 中的一個不透明類別,客戶端可以使用它在同一個位址空間內同時操作 LLVM 的多個隔離實例。例如,在一個假設的編譯伺服器中,個別翻譯單元的編譯在概念上獨立於所有其他單元,並且期望能夠在獨立的伺服器執行緒上同時編譯傳入的翻譯單元。幸運的是,LLVMContext
的存在正是為了實現這種情境!
從概念上講,LLVMContext
提供了隔離。LLVM 記憶體 IR 中的每個 LLVM 實體(Module
、Value
、Type
、Constant
等)都屬於一個 LLVMContext
。不同上下文中的實體 *不能* 相互交互:不同上下文中的 Module
無法連結在一起,Function
無法添加到不同上下文中的 Module
等。這意味著在多個執行緒上同時編譯是安全的,只要沒有兩個執行緒在同一個上下文中的實體上操作即可。
實際上,API 中很少有地方需要顯式指定 LLVMContext
,除了 Type
建立/查找 API 之外。由於每個 Type
都攜帶對其所有者上下文的參考,因此大多數其他實體都可以透過查看它們自己的 Type
來確定它們所屬的上下文。如果您要向 LLVM IR 添加新實體,請嘗試保持此介面設計。
執行緒和 JIT (Threads and the JIT)¶
LLVM 的「急切」JIT 編譯器可以安全地在執行緒化程式中使用。多個執行緒可以同時調用 ExecutionEngine::getPointerToFunction()
或 ExecutionEngine::runFunction()
,並且多個執行緒可以同時執行 JIT 輸出的程式碼。使用者仍然必須確保只有一個執行緒可以訪問給定 LLVMContext
中的 IR,而另一個執行緒可能正在修改它。一種方法是在 JIT 外部訪問 IR 時始終持有 JIT 鎖定(JIT 透過添加 CallbackVH
*修改* IR)。另一種方法是僅從 LLVMContext
的執行緒調用 getPointerToFunction()
。
當 JIT 設定為延遲編譯時 (使用 ExecutionEngine::DisableLazyCompilation(false)
),目前在延遲即時編譯函式後更新呼叫點時,存在競爭條件。如果確保一次只有一個執行緒可以呼叫任何特定的延遲 Stub,並且 JIT 鎖保護任何 IR 存取,則仍然可以在多執行緒程式中使用延遲 JIT,但我們建議在多執行緒程式中僅使用積極 JIT。
進階主題¶
本節描述了一些大多數客戶端不需要注意的進階或晦澀的 API。這些 API 傾向於管理 LLVM 系統的內部運作,僅在不尋常的情況下才需要存取。
ValueSymbolTable
類別¶
ValueSymbolTable
(doxygen) 類別提供了一個符號表,Function 和 Module 類別使用它來命名值定義。符號表可以為任何 Value 提供名稱。
請注意,大多數客戶端不應直接存取 SymbolTable
類別。它僅應在需要迭代符號表名稱本身時使用,這是一種非常特殊的用途。請注意,並非所有 LLVM Value 都有名稱,並且那些沒有名稱的 (即它們具有空名稱) 不存在於符號表中。
符號表支援使用 begin/end/iterator
迭代符號表中的值,並支援查詢以查看特定名稱是否在符號表中 (使用 lookup
)。ValueSymbolTable
類別不公開任何公共 mutator 方法,而是直接在值上呼叫 setName
,這將自動將其插入到適當的符號表中。
User
和擁有的 Use
類別的記憶體佈局¶
User
(doxygen) 類別為表達 User
對其他 Value 實例 的所有權提供了基礎。Use
(doxygen) 輔助類別用於執行簿記並促進 O(1) 新增和移除。
User
和 Use
物件之間的互動和關係¶
User
的子類別可以選擇將其 Use
物件合併,或透過指標以 out-of-line 方式引用它們。混合變體 (某些 Use
是 inline 的,其他則是 hung off 的) 是不切實際的,並且破壞了屬於同一個 User
的 Use
物件形成連續陣列的不變性。
我們在 User
(子) 類別中有 2 種不同的佈局
佈局 a)
Use
物件位於User
物件內部 (或在固定偏移量處),並且它們的數量是固定的。佈局 b)
Use
物件由從User
物件指向陣列的指標引用,並且它們的數量可能是可變的。
截至 v2.4,每個佈局仍然擁有指向 Use
陣列開頭的直接指標。雖然對於佈局 a) 不是強制性的,但為了簡單起見,我們堅持這種冗餘。User
物件也儲存了它擁有的 Use
物件的數量。(理論上,給定下面提出的方案,也可以計算出此資訊。)
特殊形式的配置運算子 (operator new
) 強制執行以下記憶體佈局
佈局 a) 由
Use[]
陣列在User
物件之前建模。...---.---.---.---.-------... | P | P | P | P | User '''---'---'---'---'-------'''
佈局 b) 由指向
Use[]
陣列建模。.-------... | User '-------''' | v .---.---.---.---... | P | P | P | P | '---'---'---'---'''
(在上面的圖中 ‘P
’ 代表 Use**
,它儲存在成員 Use::Prev
中的每個 Use
物件中)
設計型別層級結構和多型介面¶
在 C++ 程式中,有兩種不同的設計模式傾向於導致在型別層級結構中使用虛擬派送方法。第一種是真正的型別層級結構,其中層級結構中不同的型別對特定功能和語義子集進行建模,並且這些型別嚴格地相互巢狀。在 Value
或 Type
型別層級結構中可以看到很好的例子。
第二種是希望跨多型介面實作集合動態派送。後一種用例可以使用虛擬派送和繼承來建模,方法是定義一個抽象介面基底類別,所有實作都從該類別派生並覆寫。但是,這種實作策略強制存在一個實際上沒有意義的 “is-a” 關係。通常不存在程式碼可能與之互動並上下移動的有用泛化巢狀層級結構。相反,存在一個單一介面,該介面跨一系列實作進行派送。
第二種用例的首選實作策略是泛型程式設計 (有時稱為 “編譯時鴨子型別” 或 “靜態多型”)。例如,類型參數 T
上的模板可以跨任何符合介面或概念的特定實例化。這裡的一個很好的例子是任何在有向圖中建模節點的類型的高度通用屬性。LLVM 主要透過模板和泛型程式設計對這些進行建模。此類模板包括 LoopInfoBase
和 DominatorTreeBase
。當這種多型性真正需要動態派送時,您可以使用稱為基於概念的多型的技術來泛化它。這種模式使用非常有限形式的虛擬派送進行類型消除,在其內部實作中模擬模板的介面和行為。您可以在 PassManager.h
系統中找到此技術的範例,並且 Sean Parent 在他的幾次演講和論文中對其進行了更詳細的介紹
繼承是萬惡之基 - GoingNative 2013 演講描述了這項技術,可能是最好的起點。
值語義和基於概念的多型 - C++Now! 2012 演講更詳細地描述了這項技術。
Sean Parent 的論文和簡報 - 連結到投影片、影片,有時還有程式碼。
在建立型別層級結構 (使用標籤或虛擬派送) 和使用模板或基於概念的多型之間進行決策時,請考慮抽象基底類別的改進是否是介面邊界上語義上有意義的型別。如果任何比根抽象介面更精細的東西作為語義模型的局部擴展而言都是沒有意義的,那麼您的用例可能更適合多型,您應該避免使用虛擬派送。但是,可能存在一些緊急情況需要使用一種或另一種技術。
如果您確實需要引入型別層級結構,我們更喜歡使用具有手動標籤派送和/或 RTTI 的顯式封閉型別層級結構,而不是在 C++ 程式碼中更常見的開放繼承模型和虛擬派送。這是因為 LLVM 很少鼓勵程式庫消費者擴展其核心型別,並利用其層級結構的封閉和標籤派送性質來產生顯著更有效率的程式碼。我們還發現,我們對型別層級結構的大量使用更適合基於標籤的模式匹配,而不是跨通用介面的動態派送。在 LLVM 內部,我們建立了自訂輔助程式來促進此設計。請參閱本文檔中關於 isa 和 dyn_cast 的章節以及我們的 詳細文檔,其中描述了如何實作此模式以與 LLVM 輔助程式一起使用。
ABI 破壞性檢查¶
更改 LLVM C++ ABI 的檢查和斷言基於預處理器符號 LLVM_ENABLE_ABI_BREAKING_CHECKS – 使用 LLVM_ENABLE_ABI_BREAKING_CHECKS 建置的 LLVM 程式庫與未定義它的 LLVM 程式庫 ABI 不相容。預設情況下,開啟斷言也會開啟 LLVM_ENABLE_ABI_BREAKING_CHECKS,因此預設 +Asserts 建置與預設 -Asserts 建置 ABI 不相容。想要在 +Asserts 和 -Asserts 建置之間實現 ABI 相容性的客戶端應使用 CMake 建置系統來獨立於 LLVM_ENABLE_ASSERTIONS 設定 LLVM_ENABLE_ABI_BREAKING_CHECKS。
核心 LLVM 類別層級參考¶
#include "llvm/IR/Type.h"
標頭來源:Type.h
doxygen 資訊:Type 類別
核心 LLVM 類別是表示正在檢查或轉換的程式的主要方法。核心 LLVM 類別在 include/llvm/IR
目錄中的標頭檔中定義,並在 lib/IR
目錄中實作。值得注意的是,由於歷史原因,此程式庫稱為 libLLVMCore.so
,而不是您可能期望的 libLLVMIR.so
。
Type
類別和衍生型別¶
Type
是所有型別類別的超類別。每個 Value
都有一個 Type
。Type
無法直接實例化,只能透過其子類別實例化。某些原始型別 (VoidType
、LabelType
、FloatType
和 DoubleType
) 具有隱藏的子類別。它們被隱藏是因為除了 Type
類別提供的功能外,它們沒有提供任何有用的功能,只是將自己與 Type
的其他子類別區分開來。
所有其他型別都是 DerivedType
的子類別。型別可以命名,但這不是必需的。在任何一個時間點,給定形狀都只存在一個實例。這允許使用型別實例的位址相等性來執行型別相等性。也就是說,給定兩個 Type*
值,如果指標相同,則型別相同。
重要的公共方法¶
bool isIntegerTy() const
: 對於任何整數型別,傳回 true。bool isFloatingPointTy()
: 如果這是五個浮點型別之一,則傳回 true。bool isSized()
: 如果型別具有已知大小,則傳回 true。沒有大小的東西是抽象型別、標籤和 void。
重要的衍生型別¶
IntegerType
DerivedType 的子類別,表示任何位元寬度的整數型別。可以表示
IntegerType::MIN_INT_BITS
(1) 和IntegerType::MAX_INT_BITS
(~8 百萬) 之間的任何位元寬度。static const IntegerType* get(unsigned NumBits)
: 取得特定位元寬度的整數型別。unsigned getBitWidth() const
: 取得整數型別的位元寬度。
SequentialType
這是 ArrayType 和 VectorType 的子類別。
const Type * getElementType() const
: 傳回序列型別中每個元素的型別。uint64_t getNumElements() const
: 傳回序列型別中元素的數量。
ArrayType
這是 SequentialType 的子類別,並定義了陣列型別的介面。
PointerType
指標型別的 Type 子類別。
VectorType
向量型別的 SequentialType 子類別。向量型別與 ArrayType 類似,但區別在於它是第一類型別,而 ArrayType 不是。向量型別用於向量運算,通常是整數或浮點型別的小向量。
StructType
結構型別的 DerivedTypes 子類別。
FunctionType
函式型別的 DerivedTypes 子類別。
bool isVarArg() const
: 如果是變長參數函式,則傳回 true。const Type * getReturnType() const
: 傳回函式的傳回型別。const Type * getParamType (unsigned i)
: 傳回第 i 個參數的型別。const unsigned getNumParams() const
: 傳回形式參數的數量。
Module
類別¶
#include "llvm/IR/Module.h"
標頭來源:Module.h
doxygen 資訊:Module 類別
Module
類別表示 LLVM 程式中存在的頂層結構。LLVM 模組實際上是原始程式的翻譯單元,或者是連結器合併的多個翻譯單元的組合。Module
類別追蹤 Function 的列表、GlobalVariable 的列表和 SymbolTable。此外,它還包含一些有用的成員函式,試圖使常見操作變得容易。
Module
類別的重要公共成員¶
Module::Module(std::string name = "")
建構 Module 很簡單。您可以選擇性地為其提供名稱 (可能基於翻譯單元的名稱)。
Module::iterator
- 函式列表迭代器的 TypedefModule::const_iterator
- const_iterator 的 Typedef。begin()
,end()
,size()
,empty()
這些是轉發方法,可以輕鬆存取
Module
物件的 Function 列表的內容。Module::FunctionListType &getFunctionList()
傳回 Function 的列表。當您需要更新列表或執行沒有轉發方法的複雜操作時,這是必要的。
Module::global_iterator
- 全域變數列表迭代器的 TypedefModule::const_global_iterator
- const_iterator 的 Typedef。Module::insertGlobalVariable()
- 將全域變數插入到列表中。Module::removeGlobalVariable()
- 從列表中移除全域變數。Module::eraseGlobalVariable()
- 從列表中移除全域變數並將其刪除。global_begin()
,global_end()
,global_size()
,global_empty()
這些是轉發方法,可以輕鬆存取
Module
物件的 GlobalVariable 列表的內容。
SymbolTable *getSymbolTable()
傳回對此
Module
的 SymbolTable 的參考。
Function *getFunction(StringRef Name) const
在
Module
SymbolTable 中查找指定的函式。如果它不存在,則傳回null
。FunctionCallee getOrInsertFunction(const std::string &Name, const FunctionType *T)
在
Module
SymbolTable 中查找指定的函式。如果它不存在,則為函式新增外部宣告並傳回它。請注意,已經存在的函式簽章可能與請求的簽章不符。因此,為了啟用將結果直接傳遞給 EmitCall 的常見用法,傳回型別是{FunctionType *T, Constant *FunctionPtr}
的結構,而不是僅僅具有可能意外簽章的Function*
。std::string getTypeName(const Type *Ty)
如果 SymbolTable 中指定的 Type 至少有一個條目,則傳回它。否則傳回空字串。
bool addTypeName(const std::string &Name, const Type *Ty)
在 SymbolTable 中插入將
Name
映射到Ty
的條目。如果此名稱已存在條目,則傳回 true,並且不修改 SymbolTable。
Value
類別¶
#include "llvm/IR/Value.h"
標頭來源:Value.h
doxygen 資訊:Value 類別
Value
類別是 LLVM 原始程式碼庫中最重要的類別。它表示一個型別化的值,該值可以用作 (除其他外) 指令的運算元。有許多不同型別的 Value
,例如 Constant、Argument。甚至 Instruction 和 Function 也是 Value
。
一個特定的 Value
可以在程式的 LLVM 表示中使用多次。例如,函式的傳入引數 (用 Argument 類別的實例表示) 被函式中引用該引數的每個指令 “使用”。為了追蹤這種關係,Value
類別保留了正在使用它的所有 User
的列表 (User 類別是可以引用 Value
的 LLVM 圖中所有節點的基底類別)。此使用列表是 LLVM 如何在程式中表示 def-use 資訊的方式,並且可以透過下面顯示的 use_*
方法存取。
由於 LLVM 是一種型別化的表示,因此每個 LLVM Value
都是型別化的,並且此 Type 可透過 getType()
方法取得。此外,所有 LLVM 值都可以命名。Value
的 “名稱” 是在 LLVM 程式碼中列印的符號字串
%foo = add i32 1, 2
此指令的名稱為 “foo”。注意,任何值的名稱都可能遺失 (空字串),因此名稱僅應用於偵錯 (使原始程式碼更易於閱讀、偵錯列印輸出),它們不應用於追蹤值或在它們之間進行映射。為此,請改用指向 Value
本身的指標的 std::map
。
LLVM 的一個重要方面是,SSA 變數與產生它的運算之間沒有區別。因此,對指令產生的值 (或例如作為傳入引數可用的值) 的任何引用都表示為指向表示此值的類別實例的直接指標。雖然這可能需要一些時間來適應,但它簡化了表示形式並使其更易於操作。
Value
類別的重要公共成員¶
Value::use_iterator
- use-list 上迭代器的 TypedefValue::const_use_iterator
- use-list 上 const_iterator 的 Typedefunsigned use_size()
- 傳回值的使用者數量。bool use_empty()
- 如果沒有使用者,則傳回 true。use_iterator use_begin()
- 取得指向 use-list 開頭的迭代器。use_iterator use_end()
- 取得指向 use-list 結尾的迭代器。User *use_back()
- 傳回列表中的最後一個元素。這些方法是存取 LLVM 中 def-use 資訊的介面。與 LLVM 中的所有其他迭代器一樣,命名約定遵循 STL 定義的約定。
Type *getType() const
此方法傳回 Value 的 Type。bool hasName() const
std::string getName() const
void setName(const std::string &Name)
此方法系列用於存取和為
Value
指派名稱,請注意上面的 預防措施。void replaceAllUsesWith(Value *V)
此方法遍歷
Value
的 use list,將目前值的所有 User 更改為引用 “V
”。例如,如果您偵測到指令始終產生常數值 (例如透過常數摺疊),則可以像這樣將指令的所有用途替換為常數Inst->replaceAllUsesWith(ConstVal);
User
類別¶
#include "llvm/IR/User.h"
標頭來源:User.h
doxygen 資訊:User 類別
超類別:Value
User
類別是可以引用 Value
的所有 LLVM 節點的通用基底類別。它公開了 “Operands” 列表,這些列表是 User 正在引用的所有 Value
。User
類別本身是 Value
的子類別。
User
的運算元直接指向它引用的 LLVM Value
。由於 LLVM 使用靜態單賦值 (SSA) 形式,因此只能引用一個定義,從而允許這種直接連線。此連線提供 LLVM 中的 use-def 資訊。
User
類別的重要公共成員¶
User
類別透過兩種方式公開運算元列表:透過索引存取介面和透過基於迭代器的介面。
Value *getOperand(unsigned i)
unsigned getNumOperands()
這兩種方法以方便直接存取的形式公開
User
的運算元。User::op_iterator
- 運算元列表上迭代器的 Typedefop_iterator op_begin()
- 取得指向運算元列表開頭的迭代器。op_iterator op_end()
- 取得指向運算元列表結尾的迭代器。這些方法共同構成了
User
的運算元的基於迭代器的介面。
Instruction
類別¶
#include "llvm/IR/Instruction.h"
標頭來源:Instruction.h
doxygen 資訊:Instruction 類別
Instruction
類別是所有 LLVM 指令的通用基底類別。它僅提供少數方法,但它是一個非常常用的類別。Instruction
類別本身追蹤的主要資料是不透明碼 (指令型別) 和 Instruction
嵌入到的父 BasicBlock。為了表示特定型別的指令,使用了 Instruction
的許多子類別之一。
因為 Instruction
類別是 User 類別的子類別,所以其運算元可以像其他 User
一樣存取(使用 getOperand()
/getNumOperands()
和 op_begin()
/op_end()
方法)。Instruction
類別的一個重要檔案是 llvm/Instruction.def
檔案。此檔案包含關於 LLVM 中各種不同指令類型的元數據。它描述了用作運算碼的枚舉值(例如 Instruction::Add
和 Instruction::ICmp
),以及實作指令的 Instruction
的具體子類別(例如 BinaryOperator 和 CmpInst)。不幸的是,由於在此檔案中使用巨集會混淆 doxygen,因此這些枚舉值無法在 doxygen 輸出中正確顯示。
Instruction
類別的重要子類別¶
BinaryOperator
此子類別代表所有兩個運算元的指令,這些指令的運算元必須是相同類型,比較指令除外。
CastInst
此子類別是 12 個轉型指令的父類別。它提供了關於轉型指令的通用操作。
Instruction
類別的重要公開成員¶
BasicBlock *getParent()
傳回此
Instruction
嵌入的 BasicBlock。bool mayWriteToMemory()
如果指令寫入記憶體,則傳回 true,即它是
call
、free
、invoke
或store
。unsigned getOpcode()
傳回
Instruction
的運算碼。Instruction *clone() const
傳回指定指令的另一個實例,在所有方面都與原始指令相同,除了該指令沒有父項(即,它沒有嵌入到 BasicBlock 中),並且它沒有名稱。
Constant
類別及其子類別¶
Constant 代表不同類型常數的基底類別。它由 ConstantInt、ConstantArray 等子類別化,以代表各種常數類型。GlobalValue 也是一個子類別,它代表全域變數或函數的位址。
Constant 的重要子類別¶
ConstantInt:此 Constant 的子類別代表任何寬度的整數常數。
const APInt& getValue() const
:傳回此常數的底層值,一個 APInt 值。int64_t getSExtValue() const
:透過符號擴展將底層 APInt 值轉換為 int64_t。如果 APInt 的值(而非位元寬度)太大而無法容納在 int64_t 中,則會導致斷言失敗。因此,不建議使用此方法。uint64_t getZExtValue() const
:透過零擴展將底層 APInt 值轉換為 uint64_t。如果 APInt 的值(而非位元寬度)太大而無法容納在 uint64_t 中,則會導致斷言失敗。因此,不建議使用此方法。static ConstantInt* get(const APInt& Val)
:傳回代表Val
提供的值的 ConstantInt 物件。類型隱含為與Val
的位元寬度對應的 IntegerType。static ConstantInt* get(const Type *Ty, uint64_t Val)
:傳回代表整數類型Ty
的Val
提供的值的 ConstantInt 物件。
ConstantFP:此類別代表浮點數常數。
double getValue() const
:傳回此常數的底層值。
ConstantArray:這代表常數陣列。
const std::vector<Use> &getValues() const
:傳回組成此陣列的元件常數向量。
ConstantStruct:這代表常數結構。
const std::vector<Use> &getValues() const
:傳回組成此陣列的元件常數向量。
GlobalValue:這代表全域變數或函數。在任何一種情況下,該值都是一個固定的常數位址(在連結之後)。
GlobalValue
類別¶
#include "llvm/IR/GlobalValue.h"
標頭來源:GlobalValue.h
doxygen 資訊:GlobalValue 類別
全域值(GlobalVariable 或 Function)是唯一在所有 Function 的主體中可見的 LLVM 值。因為它們在全域範圍內可見,所以它們也受與不同翻譯單元中定義的其他全域變數連結的約束。為了控制連結過程,GlobalValue
知道其連結規則。具體來說,GlobalValue
知道它們是否具有內部或外部連結,如 LinkageTypes
枚舉所定義。
如果 GlobalValue
具有內部連結(相當於 C 中的 static
),則它對目前翻譯單元之外的程式碼不可見,並且不參與連結。如果它具有外部連結,則它對外部程式碼可見,並且參與連結。除了連結資訊之外,GlobalValue
還追蹤它們目前屬於哪個 Module。
因為 GlobalValue
是記憶體物件,所以它們始終由其位址引用。因此,全域變數的 Type 始終是指向其內容的指標。在使用 GetElementPtrInst
指令時,記住這一點非常重要,因為必須先取消引用此指標。例如,如果您有一個 GlobalVariable
(GlobalValue)
的子類別),它是一個包含 24 個整數的陣列,類型為 [24 x i32]
,那麼 GlobalVariable
是指向該陣列的指標。雖然此陣列的第一個元素的位址和 GlobalVariable
的值相同,但它們具有不同的類型。GlobalVariable
的類型是 [24 x i32]
。第一個元素的類型是 i32.
因此,存取全域值需要您先使用 GetElementPtrInst
取消引用指標,然後才能存取其元素。這在 LLVM 語言參考手冊中進行了解釋。
GlobalValue
類別的重要公開成員¶
bool hasInternalLinkage() const
bool hasExternalLinkage() const
void setInternalLinkage(bool HasInternalLinkage)
這些方法操作
GlobalValue
的連結特性。Module *getParent()
這會傳回 GlobalValue 目前嵌入的 Module。
Function
類別¶
#include "llvm/IR/Function.h"
標頭來源:Function.h
doxygen 資訊:Function 類別
超類別:GlobalValue、Constant、User、Value
Function
類別代表 LLVM 中的單個程序。它實際上是 LLVM 階層結構中更複雜的類別之一,因為它必須追蹤大量資料。Function
類別追蹤 BasicBlock 的列表、形式 Argument 的列表和 SymbolTable。
BasicBlock 的列表是 Function
物件最常用的部分。該列表對函數中的區塊施加了隱式排序,這表明後端將如何佈局程式碼。此外,第一個 BasicBlock 是 Function
的隱式入口節點。在 LLVM 中,顯式分支到此初始區塊是不合法的。沒有隱式出口節點,事實上,單個 Function
可能有多個出口節點。如果 BasicBlock 列表為空,則表示 Function
實際上是函數宣告:函數的實際主體尚未連結進來。
除了 BasicBlock 的列表之外,Function
類別還追蹤函數接收的形式 Argument 的列表。此容器管理 Argument 節點的生命週期,就像 BasicBlock 列表對 BasicBlock 所做的那樣。
SymbolTable 是一個非常少用的 LLVM 功能,僅當您必須按名稱查找值時才使用。除此之外,SymbolTable 在內部用於確保函數主體中的 Instruction、BasicBlock 或 Argument 的名稱之間沒有衝突。
請注意,Function
是一個 GlobalValue,因此也是一個 Constant。函數的值是其位址(在連結之後),保證是常數。
Function
的重要公開成員¶
Function(const FunctionType *Ty, LinkageTypes Linkage, const std::string &N = "", Module* Parent = 0)
當您需要建立新的
Function
以新增程式時使用的建構函式。建構函式必須指定要建立的函數的類型以及函數應具有的連結類型。FunctionType 引數指定函數的形式引數和傳回值。相同的 FunctionType 值可用於建立多個函數。Parent
引數指定定義函數的 Module。如果提供了此引數,則函數將自動插入到該模組的函數列表中。bool isDeclaration()
傳回
Function
是否已定義主體。如果函數是「外部的」,則它沒有主體,因此必須透過與在不同翻譯單元中定義的函數連結來解析。Function::iterator
- 基本區塊列表迭代器的 TypedefFunction::const_iterator
- const_iterator 的 Typedef。begin()
、end()
、size()
、empty()
、insert()
、splice()
、erase()
這些是轉發方法,可輕鬆存取
Function
物件的 BasicBlock 列表的內容。Function::arg_iterator
- 引數列表迭代器的 TypedefFunction::const_arg_iterator
- const_iterator 的 Typedef。arg_begin()
、arg_end()
、arg_size()
、arg_empty()
這些是轉發方法,可輕鬆存取
Function
物件的 Argument 列表的內容。Function::ArgumentListType &getArgumentList()
傳回 Argument 的列表。當您需要更新列表或執行沒有轉發方法的複雜操作時,這是必要的。
BasicBlock &getEntryBlock()
傳回函數的入口
BasicBlock
。由於函數的入口區塊始終是第一個區塊,因此這會傳回Function
的第一個區塊。Type *getReturnType()
FunctionType *getFunctionType()
這會遍歷
Function
的 Type,並傳回函數的傳回類型,或實際函數的 FunctionType。SymbolTable *getSymbolTable()
傳回指向此
Function
的 SymbolTable 的指標。
GlobalVariable
類別¶
#include "llvm/IR/GlobalVariable.h"
標頭來源:GlobalVariable.h
doxygen 資訊:GlobalVariable 類別
超類別:GlobalValue、Constant、User、Value
全域變數由(驚喜!)GlobalVariable
類別表示。與函數一樣,GlobalVariable
也是 GlobalValue 的子類別,因此始終由其位址引用(全域值必須存在於記憶體中,因此它們的「名稱」指的是它們的常數位址)。有關更多資訊,請參閱 GlobalValue。全域變數可能具有初始值(必須是 Constant),如果它們具有初始化器,則它們本身可以標記為「常數」(表示它們的內容在執行階段永遠不會更改)。
GlobalVariable
類別的重要公開成員¶
GlobalVariable(const Type *Ty, bool isConstant, LinkageTypes &Linkage, Constant *Initializer = 0, const std::string &Name = "", Module* Parent = 0)
建立指定類型的新全域變數。如果
isConstant
為 true,則全域變數將標記為在程式中不會變更。Linkage 參數指定變數的連結類型(內部、外部、弱連結、linkonce、附加)。如果連結是 InternalLinkage、WeakAnyLinkage、WeakODRLinkage、LinkOnceAnyLinkage 或 LinkOnceODRLinkage,則產生的全域變數將具有內部連結。AppendingLinkage 將變數的所有實例(在不同的翻譯單元中)串連在一起成為單個變數,但僅適用於陣列。有關連結類型的更多詳細資訊,請參閱 LLVM 語言參考。您可以選擇性地為全域變數指定初始化器、名稱和要將變數放入的模組。bool isConstant() const
如果這是已知在執行階段不會修改的全域變數,則傳回 true。
bool hasInitializer()
如果此
GlobalVariable
具有初始化器,則傳回 true。Constant *getInitializer()
傳回
GlobalVariable
的初始值。如果沒有初始化器,則呼叫此方法是不合法的。
BasicBlock
類別¶
#include "llvm/IR/BasicBlock.h"
標頭來源:BasicBlock.h
doxygen 資訊:BasicBlock 類別
超類別:Value
此類別代表程式碼的單一入口單一出口部分,編譯器社群通常將其稱為基本區塊。BasicBlock
類別維護 Instruction 的列表,這些指令構成區塊的主體。與語言定義相符,此指令列表的最後一個元素始終是終止符指令。
除了追蹤構成區塊的指令列表之外,BasicBlock
類別還追蹤它嵌入的 Function。
請注意,BasicBlock
本身是 Value,因為它們被分支等指令引用,並且可以進入 switch 表。BasicBlock
的類型為 label
。
BasicBlock
類別的重要公開成員¶
BasicBlock(const std::string &Name = "", Function *Parent = 0)
BasicBlock
建構函式用於建立新的基本區塊以插入到函數中。建構函式可選擇性地採用新區塊的名稱和要將其插入到的 Function。如果指定了Parent
參數,則新的BasicBlock
會自動插入到指定的 Function 的末尾;如果未指定,則必須將 BasicBlock 手動插入到 Function 中。BasicBlock::iterator
- 指令列表迭代器的 TypedefBasicBlock::const_iterator
- const_iterator 的 Typedef。begin()
、end()
、front()
、back()
、size()
、empty()
、splice()
用於存取指令列表的 STL 樣式函數。這些方法和 typedef 是轉發函數,它們具有與同名的標準函式庫方法相同的語意。這些方法以易於操作的方式公開基本區塊的底層指令列表。
Function *getParent()
傳回指向區塊嵌入的 Function 的指標,如果它是無家可歸的,則傳回空指標。
Instruction *getTerminator()
傳回指向出現在
BasicBlock
末尾的終止符指令的指標。如果沒有終止符指令,或者區塊中的最後一個指令不是終止符,則傳回空指標。
Argument
類別¶
Value 的此子類別定義了函數的傳入形式引數的介面。Function 維護其形式引數的列表。引數具有指向父 Function 的指標。