LLVM 程式設計師手冊

警告

這始終是一項進行中的工作。

簡介

本文件旨在重點介紹 LLVM 原始碼庫中提供的一些重要類別和介面。本手冊並非旨在解釋 LLVM 是什麼、它如何運作以及 LLVM 程式碼的外觀。它假設您了解 LLVM 的基礎知識,並且有興趣編寫轉換或以其他方式分析或操作程式碼。

本文件應該讓您熟悉 LLVM 基礎架構中不斷增長的原始碼,以便您找到自己的方向。請注意,本手冊並非旨在取代閱讀原始碼,因此如果您認為其中一個類別中應該有一個方法可以執行某項操作,但它並未列出,請檢查原始碼。提供指向 doxygen 原始碼的連結,以盡可能簡化此過程。

本文件的第一部分描述在 LLVM 基礎架構中工作時需要了解的通用資訊,第二部分描述核心 LLVM 類別。未來,本手冊將擴展更多資訊,說明如何使用擴展函式庫,例如支配器資訊、CFG 遍歷常式和實用的工具程式,例如 InstVisitor (doxygen) 範本。

一般資訊

本節包含在 LLVM 原始碼庫中工作時有用的通用資訊,但這些資訊並非特定於任何特定的 API。

C++ 標準模板庫

LLVM 大量使用 C++ 標準模板庫 (STL),可能比您習慣或以前見過的還要多。因此,您可能需要閱讀一些有關 STL 所使用技術和功能的背景資料。有許多優質的網頁討論 STL,以及您可以取得的幾本關於這個主題的書籍,因此本文檔將不對此進行討論。

以下是一些有用的連結

  1. cppreference.com - STL 和標準 C++ 函式庫其他部分的絕佳參考。

  2. cplusplus.com - 另一個與上述類似的絕佳參考。

  3. C++ In a Nutshell - 這是一本正在製作中的 O’Reilly 書籍。它有一個不錯的標準函式庫參考,可與 Dinkumware 的相媲美,而且自從這本書出版以來,不幸的是不再免費。

  4. C++ 常見問題.

  5. Bjarne Stroustrup 的 C++ 網頁.

  6. Bruce Eckel 的 Thinking in C++,第二版。第 2 冊。(更好的是,購買這本書).

也鼓勵您查看 LLVM 編碼標準 指南,該指南重點關注如何編寫可維護的程式碼,而不是將大括號放在哪裡。

其他有用的參考

  1. 跨平台使用靜態和共用函式庫

重要且實用的 LLVM API

這裡我們重點介紹一些在編寫轉換時通常很有用且值得了解的 LLVM API。

isa<>cast<>dyn_cast<> 範本

LLVM 原始碼庫大量使用自訂形式的 RTTI。這些模板與 C++ dynamic_cast<> 運算子有許多相似之處,但它們沒有某些缺點(主要是因為 dynamic_cast<> 僅適用於具有虛擬函式表的類別)。由於它們經常被使用,因此您必須知道它們的作用和工作原理。所有這些模板都在 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<> 運算子相同,但它允許空指針作為參數(然後傳播它)。這有時很有用,允許您將多個空檢查組合成一個。

這五個模板可以用於任何類別,無論它們是否有虛擬函式表。如果您想添加對這些模板的支持,請參閱文件如何為您的類別層次結構設置 LLVM 風格的 RTTI

傳遞字串(StringRefTwine 類別)

儘管 LLVM 通常不做太多字串操作,但我們確實有一些重要的 API 會採用字串。兩個重要的例子是 Value 類別(它具有指令、函數等的名稱)和 StringMap 類別,這在 LLVM 和 Clang 中被廣泛使用。

這些是泛型類別,它們需要能夠接受可能包含嵌入式空字元的字串。因此,它們不能簡單地採用 const char *,並且採用 const std::string& 需要客戶端執行堆分配,這通常是不必要的。相反,許多 LLVM API 使用 StringRefconst 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.hdoxygen)。

您應該很少直接使用 StringRef 類別,因為它包含指向外部記憶體的指標,通常儲存該類別的實例是不安全的(除非您知道不會釋放外部儲存空間)。StringRef 在 LLVM 中足夠小巧且普遍,應該始終以傳值方式傳遞。

Twine 類別

Twine (doxygen) 類別是 API 接受串連字串的有效方式。例如,一種常見的 LLVM 模式是根據另一條指令的名稱和後綴來命名一條指令,例如

New = CmpInst::Create(..., SO->getName() + ".cmp");

Twine 類別實際上是一種輕量級的 rope,它指向臨時(堆疊分配)物件。Twine 可以隱式建構為將加號運算子應用於字串(即 C 字串、std::stringStringRef)的結果。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

自訂格式化

有兩種方法可以自訂類型的格式化行為。

  1. 為您的類型 T 提供 llvm::format_provider<T> 的模板特化,並使用適當的靜態格式方法。

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);
  }
}

這是一種很有用的擴充機制,可用於新增對使用您自己的自訂樣式選項格式化您自己的自訂類型的支援。但是,當您想要擴充用於格式化程式庫已經知道如何格式化的類型的機制時,它就沒有幫助了。為此,我們需要其他東西。

  1. 提供繼承自 llvm::FormatAdapter<T> 的**格式配接器**。

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 方法,並傳入指定的樣式。這允許提供任何類型的自訂格式,包括已經具有內建格式提供者的類型。

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 契約,代表程式本身的錯誤。我們的目標是記錄恆常式,並在執行階段違反恆常式時(提供一些基本診斷)在失敗點快速中止。

處理程式錯誤的基本工具是 assertion 和 llvm_unreachable 函式。Assertion 用於表達不變量條件,並且應包含描述不變量的訊息

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> 工具程式。可以使用 TError 建構此類型的值。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 函式也會返回一個必須檢查或傳播的錯誤值。如果傳遞給 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 函式移除錯誤類型,從而簡化控制流程。

字串錯誤

很多錯誤類型都沒有復原策略,唯一能採取的動作就是向使用者回報錯誤,讓使用者嘗試修復環境。在這種情況下,將錯誤表示為字串非常合理。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)。錯誤類型的傳染性意味著嘗試將其中一個函數更改為返回 ErrorExpected<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 標頭提供了兩個工具,使這種慣例更加簡潔:類型檢查方法 isAconsumeError 函數。

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

建立可失敗的迭代器和迭代器範圍

上面的封存檔案遍歷範例透過索引檢索封存檔案成員,但是這需要大量的樣板程式碼來進行迭代和錯誤訊息檢查。我們可以使用“可失敗的迭代器”模式來簡化這一點,它支援以下針對像封存檔案這樣的可失敗容器的自然迭代慣例:

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_itrmake_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 公用程式允許以自然的方式構造易誤迭代器(使用失敗的 incdec 操作)以及相對自然地使用 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 (文件) 類別模板表示對可調用物件的引用,以可調用的類型為模板。如果您不需要在函數返回後保留回調,那麼這是將回調傳遞給函數的理想選擇。在這種情況下,function_ref 之於 std::function 就像 StringRef 之於 std::string

function_ref<Ret(Param1, Param2, ...)> 可以從任何可以使用類型為 Param1Param2 等的參數調用並返回可以轉換為類型 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::functionfunction_ref 足夠小,因此應該始終按值傳遞。

LLVM_DEBUG() 巨集與 -debug 選項

通常在處理您的 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 選項的參數。

基於效能考量,在 LLVM 的最佳化建置 (--enable-optimized) 中無法使用 -debug-only。

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 編譯器的行為以及各種最佳化的效率。它有助於了解哪些最佳化有助於使特定程式執行得更快。

通常您可能會在某些大型程式上執行您的遍歷,並且您有興趣查看它進行特定轉換的次數。雖然您可以通過手動檢查或一些臨時方法來做到這一點,但这真的很痛苦,而且對於大型程式來說也不是很有用。使用 Statistic 類別可以很容易地追蹤這些資訊,並且計算出的資訊會以統一的方式與正在執行的其餘遍歷一起呈現。

Statistic 的使用範例很多,但其基本用法如下

像這樣定義您的統計數據

#define DEBUG_TYPE "mypassname"   // This goes after any #includes.
STATISTIC(NumXForms, "The # of times I did stuff");

STATISTIC 巨集定義了一個靜態變數,其名稱由第一個參數指定。遍歷名稱取自 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

顯然,有這麼多最佳化,為此類事情提供一個統一的框架是非常好的。讓您的遍歷很好地融入框架使其更易於維護和使用。

新增偵錯計數器以協助偵錯您的程式碼

有時,在編寫新的遍歷或嘗試追蹤錯誤時,能夠控制遍歷中的某些事情是否發生是很有用的。例如,有時最小化工具只能輕鬆地為您提供大型測試案例。您希望使用二分法自動將錯誤範圍縮小到發生的特定轉換或未發生的轉換。這就是偵錯計數器發揮作用的地方。它們提供了一個框架,用於使程式碼的某些部分僅執行特定次數。

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 中提供了一個實用程式,用於對範圍參數的開始和結束進行二元搜尋。它可以用於自動最小化 debug-counter 變數的範圍。

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 基本區塊 組成的 CFG、由 LLVM 機器基本區塊 組成的 CFG,以及 指令選擇 DAG。在許多情況下,在除錯編譯器的各個部分時,立即將這些圖表視覺化會很好。

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 中,如果您 呼叫 DAG.setGraphColor(node, "color"),則下一次 呼叫 DAG.viewGraph() 將以指定的顏色突出顯示該節點(顏色選擇可以在 顏色 中找到。)可以使用 呼叫 DAG.setGraphAttrs(node, "attributes") 提供更複雜的節點屬性(選擇可以在 圖表屬性 中找到。)如果要重新啟動並清除所有目前的圖表屬性,則可以 呼叫 DAG.clearGraphAttrs()

請注意,圖表可視化功能已從 Release 版本中編譯出來,以減少檔案大小。這意味著您需要 Debug+Asserts 或 Release+Asserts 版本才能使用這些功能。

為任務選擇正確的資料結構

LLVM 在 llvm/ADT/ 目錄中具有大量的資料結構,我們通常使用 STL 資料結構。本節介紹在選擇資料結構時應考慮的取捨。

第一步是選擇您自己的冒險:您想要循序容器、集合型容器還是映射型容器?選擇容器時,最重要的是您打算如何存取容器的演算法屬性。基於這一點,您應該使用

  • 映射型 容器,如果您需要根據另一個值有效地查找值。映射型容器還支援對包含的有效查詢(金鑰是否在地圖中)。映射型容器通常不支援有效的反向映射(值到金鑰)。如果您需要這樣做,請使用兩個映射。某些映射型容器還支援按排序順序有效地迭代金鑰。映射型容器是最昂貴的類型,只有在您需要這些功能之一時才使用它們。

  • 集合型 容器,如果您需要將一堆東西放入一個容器中,該容器會自動消除重複項。某些集合型容器支援按排序順序有效地迭代元素。集合型容器比循序容器更昂貴。

  • 循序 容器提供了新增元素的最有效方式,並跟踪它們添加到集合中的順序。它們允許重複並支援有效的迭代,但不支援基於金鑰的有效查找。

  • 字串 容器是一種專門的循序容器或參考結構,用於字元或位元組陣列。

  • 一個 位元 容器提供了一種有效的方式來儲存和對數字 ID 集合執行集合運算,同時自動消除重複項。位元容器對於要儲存的每個識別符號最多需要 1 位元。

一旦確定了適當的容器類別,就可以透過智慧地選擇類別的成員來微調記憶體使用、常數因子和存取的快取行為。請注意,常數因子和快取行為可能非常重要。例如,如果有一個向量通常只包含幾個元素(但也可能包含很多),那麼使用 SmallVectorvector 好得多。這樣做可以避免(相對)昂貴的 malloc/free 呼叫,這些呼叫會使將元素添加到容器的成本相形見絀。

序列式容器 (std::vector, std::list, 等等)

根據您的需求,可以使用各種序列式容器。請選擇本節中第一個可以滿足您需求的容器。

llvm/ADT/ArrayRef.h

llvm::ArrayRef 類別是在接受記憶體中元素序列式清單並僅從中讀取的介面中使用的首選類別。透過採用 ArrayRef,API 可以傳遞固定大小的陣列、std::vectorllvm::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 在堆疊上時最有用。

如果沒有充分的理由選擇內嵌元素數量 N,建議使用 SmallVector<T>(即省略 N)。這將會選擇適合在堆疊上分配的預設內嵌元素數量(例如,嘗試將 sizeof(SmallVector<T>) 保持在 64 位元組左右)。

SmallVector 也為 alloca 提供了一種良好、可移植且高效的替代方案。

SmallVector 相較於 std::vector 還有其他一些次要優勢,因此建議使用 SmallVector<Type, 0> 而不是 std::vector<Type>

  1. std::vector 是例外安全的,並且某些實作在 SmallVector 會移動元素時會進行複製元素的效能優化。

  2. SmallVector 理解 std::is_trivially_copyable<Type> 並積極使用 realloc。

  3. 許多 LLVM API 將 SmallVectorImpl 作為輸出參數(請參閱下面的註釋)。

  4. 在 64 位元平台上,N 等於 0 的 SmallVector 比 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,使其不要太小(否則每個頁面的指標開銷可能會變得過高),也不要太大(否則如果頁面沒有完全使用,記憶體就會浪費)。

此外,雖然像向量一樣保留了基於插入索引的元素順序,但 API 中不提供透過 begin()end() 迭代元素的功能,因為按順序存取元素會分配所有迭代的頁面,這會浪費記憶體並違背 PagedVector 的目的。

最後,提供了 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::dequestd::vector 的通用版本。與 std::vector 一樣,它提供恆定時間的隨機存取和其他類似的特性,但它也提供對列表開頭的有效存取。它不保證記憶體中元素的連續性。

作為這種額外靈活性的交換,std::deque 的常數因子成本明顯高於 std::vector。如果可能,請使用 std::vector 或更便宜的東西。

<list>

std::list 是一個效率極低的類別,很少派上用場。它會對插入的每個元素執行堆積配置,因此具有極高的常數因子,尤其是在處理小型資料類型時。 std::list 也僅支援雙向迭代,而不支援隨機存取迭代。

作為高成本的交換,std::list 支援有效存取清單的兩端(類似於 std::deque,但與 std::vectorSmallVector 不同)。此外,std::list 的迭代器失效特性比向量類別更強:將元素插入或移除清單不會使迭代器或指向清單中其他元素的指標失效。

llvm/ADT/ilist.h

ilist<T> 實作了一個「侵入式」雙向鏈結串列。之所以說是侵入式的,是因為它要求元素儲存並提供對清單中前後指標的存取權限。

ilist 具有與 std::list 相同的缺點,並且還需要針對元素類型實作 ilist_traits,但它提供了一些新穎的特性。具體來說,它可以有效地儲存多型物件,當元素被插入或從清單中移除時,會通知特性類別,並且保證 ilist 支援常數時間的拼接操作。

ilistiplist 互為 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> 公開繼承自此特性類別。

llvm/ADT/ilist_node.h

ilist_node<T> 以預設方式實作 ilist<T>(和類似容器)預期的向前和向後鏈結。

ilist_node<T> 旨在嵌入到節點類型 T 中,通常 T 公開繼承自 ilist_node<T>

哨兵

ilist 還有一些必須考慮的特性。為了成為 C++ 生態系統中的良好公民,它需要支援標準容器操作,例如 beginend 迭代器等。此外,對於非空的 ilistoperator-- 必須在 end 迭代器上正常運作。

解決此問題的唯一合理方法是與侵入式清單一起配置一個所謂的「哨兵」,作為 end 迭代器,提供指向最後一個元素的反向連結。然而,根據 C++ 慣例,在哨兵之後使用 operator++ 是非法的,而且也不得對其進行解引用。

這些限制允許 ilist 在如何分配和儲存哨兵方面有一些實現上的自由度。相應的策略由 ilist_traits<T> 決定。預設情況下,每當需要哨兵時,都會在堆積上分配一個 T

雖然預設策略在大多数情況下已足夠,但當 T 不提供預設建構函式時,它可能會失效。此外,在 ilist 的許多實例中,關聯哨兵的記憶體開銷是被浪費的。為了緩解大量且龐大的 T 哨兵的情況,有時會採用一種技巧,導致產生「幽靈哨兵」。

幽靈哨兵是透過特殊設計的 ilist_traits<T> 獲得的,它將哨兵與記憶體中的 ilist 實例重疊。使用指標算術來獲取哨兵,它相對於 ilistthis 指標。ilist 增加了一個額外的指標,作為哨兵的反向連結。這是幽靈哨兵中唯一可以合法存取的欄位。

其他序列式容器選項

可以使用其他 STL 容器,例如 std::string

還有各種 STL 轉接器類別,例如 std::queuestd::priority_queuestd::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 有一些主要的限制,這些限制使得更強大的字串容器變得有用

  1. 您不能直接將 StringRef 轉換為「const char*」,因為無法新增尾隨空字元(與各種更強大類別上的 .c_str() 方法不同)。

  2. StringRef 不擁有或保持基礎字串位元組的存活。因此,它很容易導致懸空指標,並且不適合在大多数情況下嵌入數據結構中(而是使用 std::string 或類似的東西)。

  3. 出於同樣的原因,如果方法“計算”結果字串,則 StringRef 不能用作方法的返回值。請改用 std::string。

  4. 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 來刪除重複項。如果您的使用模式具有這兩個不同的階段(插入然後查詢),並且可以與良好的 順序容器 選擇相結合,那麼這種方法非常有效。

這種組合提供了一些很好的特性:結果數據在內存中是連續的(有利於緩存局部性),分配少,易於尋址(最終向量中的迭代器只是索引或指針),並且可以使用標準二進制搜索進行有效查詢(例如,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

StringSetStringMap<char> 的一個輕量級包裝器,它允許有效地儲存和檢索唯一的字串。

在功能上類似於 SmallSet<StringRef>StringSet 也支援迭代。(迭代器解引用為 StringMapEntry<char>,因此您需要呼叫 i->getKey() 來存取 StringSet 的項目。)另一方面,StringSet 不支援範圍插入和複製建構,而 SmallSetSmallPtrSet 支援。

llvm/ADT/DenseSet.h

DenseSet 是一個簡單的二次探測雜湊表。它擅長於支援小值:它使用單個配置來儲存當前插入到集合中的所有配對。DenseSet 是一種很好的方法來唯一化不是簡單指標的小值(對指標使用 SmallPtrSet)。請注意,DenseSet 對值型別有與 DenseMap 相同的要求。

llvm/ADT/SparseSet.h

SparseSet 保存少量由中等大小的無符號鍵標識的物件。它使用大量的記憶體,但提供的操作幾乎與向量一樣快。典型的鍵是物理暫存器、虛擬暫存器或編號的基本區塊。

SparseSet 適用於需要非常快速的清除/尋找/插入/刪除以及對小集合進行快速迭代的演算法。它不適用於建構複合資料結構。

llvm/ADT/SparseMultiSet.h

SparseMultiSet 為 SparseSet 添加了多重集合行為,同時保留了 SparseSet 的理想屬性。與 SparseSet 一樣,它通常使用大量的記憶體,但提供的操作幾乎與向量一樣快。典型的鍵是物理暫存器、虛擬暫存器或編號的基本區塊。

SparseMultiSet 適用於需要非常快速的清除/尋找/插入/刪除整個集合,以及迭代共享鍵的元素集合的演算法。它通常比使用複合資料結構(例如向量向量、映射向量)更有效率。它不適用於建構複合資料結構。

llvm/ADT/FoldingSet.h

FoldingSet 是一個非常擅長對建立成本高昂或多型物件進行去重複的聚合類別。它結合了鏈式雜湊表和侵入式連結(去重複的物件需要繼承自 FoldingSetNode),並使用 SmallVector 作為其 ID 處理的一部分。

考慮一個你想為複雜物件(例如,程式碼產生器中的節點)實現「getOrCreateFoo」方法的情況。用戶端有一個關於它想要產生**什麼**的描述(它知道操作碼和所有運算元),但我們不想「新建」一個節點,然後嘗試將其插入到一個集合中,卻發現它已經存在,此時我們必須將其刪除並返回已經存在的節點。

為了支援這種用戶端風格,FoldingSet 使用 FoldingSetNodeID(它包裝了 SmallVector)執行查詢,該 ID 可用於描述我們要查詢的元素。查詢要么返回與 ID 匹配的元素,要么返回一個不透明的 ID,指示應插入的位置。ID 的建構通常不需要堆積流量。

由於 FoldingSet 使用侵入式連結,因此它可以在集合中支援多型物件(例如,您可以將 SDNode 實例與 LoadSDNodes 混合使用)。因為元素是單獨配置的,所以指向元素的指標是穩定的:插入或移除元素不會使指向其他元素的任何指標失效。

<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 類別,它預設使用指定大小的 SmallVectorSmallSet。如果您使用這個,並且如果您的集合動態地小於 N,您將節省大量的堆積流量。

llvm/ADT/UniqueVector.h

UniqueVector 類似於 SetVector,但它為插入集合中的每個元素保留一個唯一的 ID。 它在內部包含一個映射和一個向量,並且它為插入集合中的每個值分配一個唯一的 ID。

UniqueVector 非常昂貴:它的成本是維護映射和向量兩者的成本之和,它具有很高的複雜性、很高的常數因子,並且會產生大量的 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 的所有缺點。 排序向量(您不會刪除重複項)或其他一些方法幾乎總是更好。

類似映射的容器(std::map、DenseMap 等)

當您想將數據與鍵關聯時,類似映射的容器很有用。 像往常一樣,有很多不同的方法可以做到這一點。 :)

排序的「向量」

如果您的使用模式遵循嚴格的先插入後查詢方法,則您可以輕鬆地使用與 用於集合狀容器的排序向量 相同的方法。唯一的區別是您的查詢函數(使用 std::lower_bound 來獲得高效的 log(n) 查找)應該只比較鍵,而不比較鍵和值。這產生了與用於集合的排序向量相同的優勢。

llvm/ADT/StringMap.h

字串通常在映射中用作鍵,並且難以有效地支持:它們是可變長度的,在長度較長時哈希和比較效率低下,複製成本高昂等。StringMap 是一個專用的容器,旨在解決這些問題。它支持將任意範圍的字節映射到任意其他對象。

StringMap 實現使用二次探測哈希表,其中桶存儲指向堆分配的條目的指針(以及其他一些內容)。映射中的條目必須是堆分配的,因為字串是可變長度的。字串數據(鍵)和元素對象(值)存儲在同一個分配中,字串數據緊跟在元素對象之後。此容器保證“(char*)(&Value+1)”指向值的鍵字串。

StringMap 非常快,原因有以下幾個:二次探測對於查找具有很高的緩存效率,在查找元素時不會重新計算桶中字串的哈希值,StringMap 在查找值時很少需要觸摸不相關對象的內存(即使發生哈希衝突),哈希表增長不會重新計算表中已存在的字串的哈希值,並且映射中的每一對都存儲在單個分配中(字串數據與對的值存儲在同一個分配中)。

StringMap 還提供了採用字節範圍的查詢方法,因此只有在將值插入表中時才會複製字串。

但是,StringMap 的迭代順序不能保證是確定的,因此任何需要確定順序的用途都應該改用 std::map。

llvm/ADT/IndexedMap.h

IndexedMap 是一個專用的容器,用於將小的密集整數(或可以映射到小的密集整數的值)映射到其他一些類型。它在內部實現為一個向量,帶有一個映射函數,該函數將鍵映射到密集的整數範圍。

這對於 LLVM 代碼生成器中的虛擬寄存器之類的情況很有用:它們具有一個由編譯時常量(第一個虛擬寄存器 ID)偏移的密集映射。

llvm/ADT/DenseMap.h

DenseMap 是一個簡單的二次探測哈希表。它擅長支持小的鍵和值:它使用單個分配來保存當前插入到映射中的所有對。DenseMap 是一種將指針映射到指針或將其他小類型相互映射的好方法。

但是,DenseMap 有幾個方面需要注意。與 map 不同,DenseMap 中的迭代器在每次插入時都會失效。此外,由於 DenseMap 為大量的鍵/值對分配空間(默認情況下從 64 開始),如果您的鍵或值很大,它將浪費大量的空間。最後,如果尚未支持您想要的鍵,則必須為該鍵實現 DenseMapInfo 的部分特化。這是為了告訴 DenseMap 它內部需要的兩個特殊標記值(永遠不能插入到映射中)。

DenseMap 的 find_as() 方法支持使用備用鍵類型進行查找操作。這在構造正常鍵類型成本很高但比較成本很低的情況下很有用。DenseMapInfo 負責為使用的每種備用鍵類型定義適當的比較和哈希方法。

DenseMap.h 也包含了一個 SmallDenseMap 變體,它類似於 SmallVector,在元素數量未超過模板參數 N 之前不會執行堆積配置。

llvm/IR/ValueMap.h

ValueMap 是 DenseMap 的包裝器,用於將 Value*(或子類別)映射到另一種類型。當 Value 被刪除或 RAUW 時,ValueMap 會更新自身,以便新版本的鍵映射到相同的值,就像鍵是 WeakVH 一樣。您可以透過將 Config 參數傳遞給 ValueMap 模板,精確地配置此行為以及在這兩個事件中發生的其他行為。

llvm/ADT/IntervalMap.h

IntervalMap 是一種用於小型鍵和值的緊湊映射。它映射鍵區間而不是單個鍵,並且會自動合併相鄰的區間。當映射只包含幾個區間時,它們會被存儲在映射對象本身中,以避免分配。

IntervalMap 迭代器非常大,因此不應將它們作為 STL 迭代器傳遞。重量級迭代器允許使用更小的數據結構。

llvm/ADT/IntervalTree.h

llvm::IntervalTree 是一種輕量級樹數據結構,用於保存區間。它允許查找與任何給定點重疊的所有區間。目前,它不支持任何刪除或重新平衡操作。

IntervalTree 的設計是一次性設置,然後在不進行任何進一步添加的情況下進行查詢。

<map>

std::map 具有與 std::set 類似的特性:它為插入映射的每一對使用單個分配,它提供具有極大常數因子的 log(n) 查找,對映射中的每一對施加 3 個指針的空間懲罰等。

std::map 在您的鍵或值非常大時最有用,如果您需要按排序順序迭代集合,或者您需要穩定的映射迭代器(即,如果插入或刪除其他元素,它們不會失效)。

llvm/ADT/MapVector.h

MapVector<KeyT,ValueT> 提供了 DenseMap 接口的子集。主要區別在於迭代順序保證為插入順序,這使其成為對指針映射進行非確定性迭代的簡單(但有點昂貴)解決方案。

它的實現方式是將鍵映射到鍵值對向量中的索引。這提供了快速的查找和迭代,但有兩個主要缺點:鍵被存儲兩次,並且刪除元素需要線性時間。如果需要刪除元素,最好使用 remove_if() 批量刪除它們。

llvm/ADT/IntEqClasses.h

IntEqClasses 提供了小整數等價類的緊湊表示。最初,範圍 0..n-1 中的每個整數都有其自己的等價類。可以通過將兩個類代表傳遞給 join(a, b) 方法來加入類。當 findLeader() 返回相同的代表時,兩個整數屬於同一個類別。

一旦形成所有等價類別後,就可以壓縮映射,以便每個整數 0..n-1 都映射到範圍 0..m-1 中的一個等價類別編號,其中 m 是等價類別的總數。 在再次編輯映射之前,必須先將其解壓縮。

llvm/ADT/ImmutableMap.h

ImmutableMap 是一種基於 AVL 樹的不可變(函數式)映射實現。 添加或刪除元素是通過 Factory 對象完成的,並會導致創建新的 ImmutableMap 對象。 如果已經存在具有給定鍵集的 ImmutableMap,則返回現有的 ImmutableMap;使用 FoldingSetNodeID 比較相等性。 添加或刪除操作的時間和空間複雜度與原始映射的大小成對數關係。

其他類似映射的容器選項

STL 提供了其他幾個選項,例如 std::multimap 和 std::unordered_map。 我們從不使用像 unordered_map 這樣的容器,因為它們通常非常昂貴(每次插入都需要 malloc)。

如果您想將一個鍵映射到多個值,std::multimap 很有用,但它具有 std::map 的所有缺點。 排序向量或其他方法幾乎總是更好。

位元儲存容器

有幾種位元儲存容器,選擇何時使用每一種容器相對簡單。

另一個選擇是 std::vector<bool>:我們不建議使用它,原因有兩個:1) 許多常用編譯器(例如,常用的 GCC 版本)中的實現效率極低,以及 2) C++ 標準委員會可能會棄用這個容器和/或以某種方式對其進行重大更改。 無論如何,請不要使用它。

BitVector

BitVector 容器提供了一組動態大小的位元供操作。 它支持單個位元設置/測試,以及集合運算。 集合運算的時間複雜度為 O(位元向量的大小),但運算是以字為單位執行的,而不是以位元為單位執行。 與其他容器相比,這使得 BitVector 在集合運算方面非常快。 當您預計設置位元的數量很多時(即密集集合),請使用 BitVector。

SmallBitVector

SmallBitVector 容器提供了與 BitVector 相同的接口,但它針對只需要少量位元(少於 25 個左右)的情況進行了優化。 它也透明地支持更大的位元數,但效率略低於普通的 BitVector,因此 SmallBitVector 應該只在較大的計數很少見的情況下使用。

目前,SmallBitVector 不支持集合運算(與、或、異或),並且它的 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(連續範圍之間間隙的對數)。

當設定位元範圍之間的間隙很大時,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";

請注意,元素是通過「參考包裝器」代理類型(參考的元組)提供的,它與結構化綁定宣告相結合,使得 LetterCount 引用範圍元素。對這些參考的任何修改都會影響 LettersCounts 的元素。

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 檢查)。

除錯

為一些核心 LLVM 程式庫提供了一些 GDB pretty printers。要使用它們,請執行以下操作(或將其添加到您的 ~/.gdbinit

source /path/to/llvm/src/utils/gdb-scripts/prettyprinters.py

啟用 print pretty 選項也很方便,可以避免將資料結構打印為一大塊文字。

常見操作的有用提示

本節介紹如何對 LLVM 程式碼執行一些非常簡單的轉換。這旨在提供所使用的常見習慣用法的示例,展示 LLVM 轉換的實際方面。

因為這是一個「操作方法」部分,所以您還應該閱讀有關您將要使用的主要類別的資訊。核心 LLVM 類別階層參考 包含您應該了解的主要類別的詳細資訊和描述。

基本檢查和遍歷常式

LLVM 編譯器基礎結構有許多不同的資料結構可以遍歷。遵循 C++ 標準範本庫的示例,用於遍歷這些不同資料結構的技術基本上都是相同的。對於可列舉的值序列,XXXbegin() 函數(或方法)返回一個指向序列開頭的迭代器,XXXend() 函數返回一個指向序列最後一個有效元素之後的迭代器,並且存在一些在兩個操作之間通用的 XXXiterator 資料類型。

因為迭代模式在程式碼表示法的許多不同方面都很常見,所以標準模板庫演算法可以用於它們,而且更容易記住如何迭代。首先,我們展示一些需要遍歷的資料結構的常見範例。其他資料結構的遍歷方式非常相似。

迭代 Function 中的 BasicBlock

擁有一個想要以某種方式轉換的 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

就像處理 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

如果您發現您經常迭代 FunctionBasicBlock 以及該 BasicBlockInstruction,則應改用 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 現在將包含由 F 指向的 Function 中的所有指令。

將迭代器轉換為類別指標(反之亦然)

有時,當您手邊只有迭代器時,抓取對類別實例的參考(或指標)會很有用。那麼,從迭代器中提取參考或指標非常簡單。假設 iBasicBlock::iterator,而 jBasicBlock::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";
}

尋找呼叫站點:一個稍微複雜的例子

假設您正在編寫一個 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;
};

迭代 def-use 和 use-def 鏈

通常,我們可能有一個 Value 類別的實例(doxygen),並且我們想要確定哪些 User 使用了該 Value。特定 Value 的所有 User 的清單稱為 *def-use* 鏈。例如,假設我們有一個名為 FFunction* 指向特定函式 foo。找到 *使用* foo 的所有指令就像迭代 F 的 *def-use* 鏈一樣簡單

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 的列表稱為 _use-def_ 鏈。類別 Instruction 的實例是常見的 User,因此我們可能想要迭代特定指令使用的所有值(即特定 Instruction 的運算元)。

Instruction *pi = ...;

for (Use &U : pi->operands()) {
  Value *v = U.get();
  // ...
}

將物件宣告為 const 是強制執行無突變演算法(例如分析等)的重要工具。為此,上述迭代器以常數形式出現,如 Value::const_use_iteratorValue::const_op_iterator。當分別在 const Value*const User* 上呼叫 use/op_begin() 時,它們會自動出現。解除參照時,它們會返回 const Use*。否則,上述模式保持不變。

迭代區塊的前驅和後繼

使用 "llvm/IR/CFG.h" 中定義的例程,迭代區塊的前驅和後繼非常容易。只需使用如下代碼即可迭代 BB 的所有前驅

#include "llvm/IR/CFG.h"
BasicBlock *BB = ...;

for (BasicBlock *Pred : predecessors(BB)) {
  // ...
}

同樣,要迭代後繼,請使用 successors

進行簡單的更改

LLVM 基礎架構中存在一些值得了解的基本轉換操作。在執行轉換時,操作基本區塊的內容是很常見的。本節描述了一些常見的操作方法,並提供了示例代碼。

創建和插入新的 Instruction

實例化指令

Instruction 的創建非常簡單:只需呼叫要實例化的指令種類的構造函數,並提供必要的參數。例如,AllocaInst 只 _需要_ 一個(指向常數的指針)Type。因此

auto *ai = new AllocaInst(Type::Int32Ty);

將創建一個 AllocaInst 實例,該實例表示在運行時在當前堆棧框架中分配一個整數。每個 Instruction 子類都可能具有不同的默認參數,這些參數會更改指令的語義,因此請參閱要實例化的 Instruction 子類的 doxygen 文檔

命名值

如果可以的話,為指令的值命名是非常有用的,因為這可以方便您對轉換進行除錯。如果您最終需要查看生成的 LLVM 機器碼,您一定會希望指令的結果都與邏輯名稱相關聯!透過為 Instruction 建構函式的 Name(預設)參數提供值,您就可以將邏輯名稱與指令在執行階段的執行結果相關聯。例如,假設我正在編寫一個轉換,它會在堆疊上動態分配空間給一個整數,並且該整數將被其他程式碼用作某種索引。為了實現這一點,我在某個 Function 的第一個 BasicBlock 的第一個點放置一個 AllocaInst,並且我打算在同一個 Function 中使用它。我可以這樣做

auto *pa = new AllocaInst(Type::Int32Ty, 0, "indexLoc");

其中 indexLoc 現在是指令執行值的邏輯名稱,它是一個指向執行階段堆疊上整數的指標。

插入指令

基本上有三種方法可以將 Instruction 插入到形成 BasicBlock 的現有指令序列中

  • 插入到 BasicBlock 的指令清單中

    給定一個 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 的執行個體插入

    使用之前的方法插入多個 Instruction 可能相當費力。 IRBuilder 是一個方便的類別,可用於將多個指令新增到 BasicBlock 的末尾或特定 Instruction 之前。它還支援常數折疊和重新命名已命名的暫存器(請參閱 IRBuilder 的範本引數)。

    以下範例示範了 IRBuilder 的一個非常簡單的用法,其中在指令 pi 之前插入了三個指令。前兩個指令是呼叫指令,第三個指令將兩個呼叫的回傳值相乘。

    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 教學

刪除指令

從構成 基本區塊 的現有指令序列中刪除指令非常簡單:只需呼叫指令的 eraseFromParent() 方法。例如:

Instruction *I = .. ;
I->eraseFromParent();

這會將指令從其包含的基本區塊中取消連結並將其刪除。如果您只想將指令從其包含的基本區塊中取消連結但不刪除它,則可以使用 removeFromParent() 方法。

將指令替換為另一個值

替換個別指令

包含「llvm/Transforms/Utils/BasicBlockUtils.h」允許使用兩個非常有用的替換函數:ReplaceInstWithValueReplaceInstWithInst

刪除指令
  • 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"));
    
替換使用者和值的多個使用

您可以使用 Value::replaceAllUsesWithUser::replaceUsesOfWith 一次更改多個使用。如需更多資訊,請分別參閱 Value 類別User 類別 的 doxygen 文件。

刪除全域變數

從模組中刪除全域變數與刪除指令一樣簡單。首先,您必須具有指向要刪除的全域變數的指標。您可以使用這個指標將其從其父級(模組)中刪除。例如:

GlobalVariable *GV = .. ;

GV->eraseFromParent();

執行緒和 LLVM

本節說明 LLVM API 與多執行緒的交互作用,包括用戶端應用程式和 JIT 中託管應用程式的部分。

請注意,LLVM 對多執行緒的支援仍然相對較新。在版本 2.5 之前,支援執行緒託管應用程式的執行,但不支援用戶端對 API 的執行緒存取。雖然現在支援這種使用案例,但用戶端 *必須* 遵守以下指定的準則,以確保在多執行緒模式下正常運作。

請注意,在類 Unix 平台上,LLVM 需要 GCC 的原子內建函式才能支援執行緒操作。如果您需要在沒有適當現代系統編譯器的平台上使用支援多執行緒的 LLVM,請考慮以單執行緒模式編譯 LLVM 和 LLVM-GCC,並使用產生的編譯器來建置具有多執行緒支援的 LLVM 副本。

使用 llvm_shutdown() 結束執行

當您使用完 LLVM API 後,應該呼叫 llvm_shutdown() 來釋放內部結構所使用的記憶體。

使用 ManagedStatic 進行延遲初始化

ManagedStatic 是 LLVM 中的一個工具類別,用於實現靜態資源的靜態初始化,例如全域類型表。在單執行緒環境中,它實現了一個簡單的延遲初始化方案。然而,當 LLVM 被編譯為支援多執行緒時,它會使用雙重檢查鎖定來實現執行緒安全的延遲初始化。

使用 LLVMContext 實現隔離

LLVMContext 是 LLVM API 中的一個不透明類別,用戶端可以使用它在同一個位址空間內同時操作多個隔離的 LLVM 實例。例如,在一個假設的編譯伺服器中,單個轉譯單元的編譯在概念上是獨立於所有其他單元的,並且希望能夠在獨立的伺服器執行緒上同時編譯傳入的轉譯單元。幸運的是,LLVMContext 的存在正是為了實現這種場景!

從概念上講,LLVMContext 提供了隔離性。LLVM 中的每個實體(ModuleValueTypeConstant 等)在記憶體中的 IR 都屬於一個 LLVMContext。不同上下文中的實體*不能*相互交互:不同上下文中的 Module 不能連結在一起,Function 不能添加到不同上下文中的 Module 中,等等。這意味著在多個執行緒上同時編譯是安全的,只要沒有兩個執行緒在同一個上下文中的實體上操作即可。

實際上,除了 Type 建立/查詢 API 之外,API 中很少有地方需要顯式指定 LLVMContext。因為每個 Type 都帶有一個對其所屬上下文的引用,所以大多數其他實體可以通過查看自身的 Type 來確定它們屬於哪個上下文。如果您要向 LLVM IR 中添加新的實體,請嘗試維持這種介面設計。

執行緒和 JIT

LLVM 的「積極式」JIT 編譯器可以安全地在多執行緒程式中使用。多個執行緒可以同時呼叫 ExecutionEngine::getPointerToFunction()ExecutionEngine::runFunction(),並且多個執行緒可以同時執行 JIT 輸出的程式碼。使用者仍然必須確保只有一個執行緒在另一個執行緒可能正在修改特定 LLVMContext 中的 IR 時存取它。一種方法是在存取 JIT 之外的 IR 時始終持有 JIT 鎖定(JIT 通過添加 CallbackVH 來*修改* IR)。另一種方法是僅從 LLVMContext 的執行緒呼叫 getPointerToFunction()

當 JIT 配置為延遲編譯時(使用 ExecutionEngine::DisableLazyCompilation(false)),在函數被延遲 JIT 編譯後更新呼叫站點時,目前存在競爭條件。如果您確保一次只有一個執行緒可以呼叫任何特定的延遲 stub 並且 JIT 鎖定保護任何 IR 存取,則仍然可以在多執行緒程式中使用延遲 JIT,但我們建議僅在多執行緒程式中使用積極式 JIT。

進階主題

本節介紹一些大多數客戶端不需要知道的進階或隱晦的 API。這些 API 傾向於管理 LLVM 系統的內部運作,並且只需要在特殊情況下存取。

ValueSymbolTable 類別

ValueSymbolTable (doxygen) 類別提供了一個符號表,FunctionModule 類別使用它來命名值定義。符號表可以為任何 Value 提供名稱。

請注意,大多數客戶端不應直接存取 SymbolTable 類別。它應該僅在需要迭代符號表名稱本身時使用,這是非常特殊的用途。請注意,並非所有 LLVM Value 都有名稱,並且沒有名稱的那些(即它們的名稱為空)不存在於符號表中。

符號表支援使用 begin/end/iterator 迭代符號表中的值,並支援查詢以查看符號表中是否存在特定名稱(使用 lookup)。ValueSymbolTable 類別不公開任何公共修改器方法,而是簡單地在值上呼叫 setName,這將自動將其插入到適當的符號表中。

User 和擁有的 Use 類別的記憶體佈局

User (doxygen) 類別為表達 User 對其他 Value instance 的所有權提供了一個基礎。Use (doxygen) 輔助類別用於進行簿記並促進 *O(1)* 的新增和刪除。

UserUse 物件之間的交互和關係

User 的子類別可以選擇將其 Use 物件合併到自身,或透過指標以離線方式引用它們。混合變體(一些 Use 內嵌,其他則掛起)是不切實際的,並且會破壞屬於同一個 UserUse 物件形成連續陣列的不變性。

我們在 User(子)類別中有 2 種不同的佈局

  • 佈局 a)

    Use 物件位於 User 物件內部(分別位於固定偏移量處),並且它們的數量是固定的。

  • 佈局 b)

    Use 物件由指向 User 物件中陣列的指標引用,並且它們的數量可能是可變的。

從 v2.4 版開始,每個佈局都擁有一個直接指向 Use 陣列開頭的指標。雖然佈局 a) 並非強制要求,但為了簡化起見,我們堅持這種冗餘。 User 物件還存儲它擁有的 Use 物件的數量。(從理論上講,給定以下方案,此信息也可以計算出來。)

特殊形式的分配運算符(operator new)強制執行以下內存佈局

  • 佈局 a) 通過在 User 物件前面加上 Use[] 陣列來建模。

    ...---.---.---.---.-------...
      | P | P | P | P | User
    '''---'---'---'---'-------'''
    
  • 佈局 b) 通過指向 Use[] 陣列來建模。

    .-------...
    | User
    '-------'''
        |
        v
        .---.---.---.---...
        | P | P | P | P |
        '---'---'---'---'''
    

(在上圖中,P代表存儲在每個 Use 物件中成員 Use::Prev 中的 Use**

設計類型層次結構和多態接口

在 C++ 程序中,有兩種不同的設計模式往往會導致對類型層次結構中的方法使用虛擬調度。第一種是真正的類型層次結構,其中層次結構中的不同類型對功能和語義的特定子集進行建模,並且這些類型嚴格地嵌套在彼此之中。可以在 ValueType 類型層次結構中看到很好的例子。

第二種是在一組多態接口實現中動態調度的需求。後一種用例可以使用虛擬調度和繼承來建模,方法是定義一個抽象接口基類,所有實現都從該基類派生並覆蓋。但是,這種實現策略強制存在一個實際上沒有意義的**「is-a」**關係。通常沒有一些有用的泛化嵌套層次結構,代碼可以與之交互並上下移動。相反,有一個單一的接口,它被調度到一系列實現中。

第二種用例的首選實作策略是泛型程式設計(有時稱為「編譯時鴨子類型」或「靜態多型」)。例如,可以針對符合介面或「概念」的任何特定實作,將某些類型參數 T 的樣板實體化。一個很好的例子是有向圖中,任何模型節點的類型的通用屬性。LLVM 主要透過樣板和泛型程式設計來為這些模型建模。這些樣板包括 LoopInfoBaseDominatorTreeBase。當這種多型真正需要「動態」分派時,您可以使用一種稱為「基於概念的多型」的技術來概括它。這種模式使用非常有限形式的虛擬分派,在其內部實現中進行類型擦除,來模擬樣板的介面和行為。您可以在 PassManager.h 系統中找到這種技術的範例,Sean Parent 在他的幾場演講和論文中對此有更詳細的介紹。

  1. 繼承是萬惡之源 - GoingNative 2013 的演講,描述了這種技術,可能是最好的入門資源。

  2. 值語義和基於概念的多型 - C++Now! 2012 的演講,更詳細地描述了這種技術。

  3. Sean Parent 的論文和簡報 - 一個 GitHub 專案,其中包含指向幻燈片、影片和程式碼的連結。

在建立類型階層(使用標記或虛擬分派)和使用樣板或基於概念的多型之間做出決定時,請考慮抽象基類的某些細化是否是在介面邊界上有語義意義的類型。如果比根抽象介面更精細的任何內容作為語義模型的部分擴充都沒有意義,那麼您的用例可能更適合多型,並且您應該避免使用虛擬分派。但是,在某些特殊情況下,可能需要使用其中一種技術。

如果您確實需要引入類型階層,我們更喜歡使用具有手動標記分派和/或 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 都有一個 TypeType 不能直接實例化,只能透過其子類別實例化。某些基本類型(VoidTypeLabelTypeFloatTypeDoubleType)具有隱藏的子類別。它們之所以被隱藏,是因為除了將自己與 Type 的其他子類別區分開來之外,它們沒有提供 Type 類別提供的任何有用功能。

所有其他類型都是 DerivedType 的子類別。類型可以被命名,但這不是必需的。在任何時候都只有一個給定形狀的實例存在。這允許使用 Type Instance 的地址相等性來執行類型相等性。也就是說,給定兩個 Type* 值,如果指標相同,則類型相同。

重要的公開方法

  • bool isIntegerTy() const:對於任何整數類型都返回 true。

  • bool isFloatingPointTy():如果這是五種浮點數類型之一,則返回 true。

  • bool isSized():如果類型具有已知大小,則返回 true。沒有大小的東西是抽象類型、標籤和 void。

重要的衍生類型

IntegerType

DerivedType 的子類別,表示任何位元寬度的整數類型。可以表示 IntegerType::MIN_INT_BITS (1) 和 IntegerType::MAX_INT_BITS(約 800 萬)之間的任何位元寬度。

  • 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 類別會追蹤 函數 清單、全域變數 清單和 符號表。此外,它還包含一些實用的成員函數,可以簡化常見的操作。

Module 類別的重要公開成員

  • Module::Module(std::string name = "")

    建構 模組 很簡單。您可以選擇性地為其提供名稱(可能基於翻譯單元的名稱)。

  • Module::iterator - 函數清單迭代器的類型定義
    Module::const_iterator - const_iterator 的類型定義。
    begin()end()size()empty()

    以下是一些轉發方法,可以輕鬆訪問 Module 物件的 Function 列表內容。

  • Module::FunctionListType &getFunctionList()

    返回 Function 的列表。當您需要更新列表或執行沒有轉發方法的複雜操作時,需要使用此方法。


  • Module::global_iterator - 全局變數列表迭代器的 Typedef
    Module::const_global_iterator - const_iterator 的 Typedef。
    Module::insertGlobalVariable() - 將全局變數插入列表中。
    Module::removeGlobalVariable() - 從列表中刪除全局變數。
    Module::eraseGlobalVariable() - 從列表中刪除全局變數並刪除它。
    global_begin(), global_end(), global_size(), global_empty()

    以下是一些轉發方法,可以輕鬆訪問 Module 物件的 GlobalVariable 列表內容。


  • SymbolTable *getSymbolTable()

    返回此 ModuleSymbolTable 的引用。


  • Function *getFunction(StringRef Name) const

    ModuleSymbolTable 中查找指定的函數。如果它不存在,則返回 null

  • FunctionCallee getOrInsertFunction(const std::string &Name, const FunctionType *T)

    ModuleSymbolTable 中查找指定的函數。如果它不存在,則為該函數添加一個外部聲明並返回它。請注意,已存在的函數簽章可能與請求的簽章不匹配。因此,為了能夠直接將結果傳遞給 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 有許多不同的類型,例如 常數參數。 甚至 指令函數 都是 Value

一個特定的 Value 可能在程式的 LLVM 表示中被使用多次。 例如,函數的輸入參數(以 Argument 類別的實例表示)會被函數中每個引用該參數的指令「使用」。 為了追蹤這種關係,Value 類別會保留一個使用它的所有 User 的清單(User 類別是 LLVM 圖表中所有可以參考 Value 的節點的基類)。 這個使用清單是 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 - 使用清單的迭代器型別定義
    Value::const_use_iterator - 使用清單的 const_iterator 型別定義
    unsigned use_size() - 傳回值的使用者數量。
    bool use_empty() - 如果沒有使用者,則傳回 true。
    use_iterator use_begin() - 取得使用清單開頭的迭代器。
    use_iterator use_end() - 取得使用清單結尾的迭代器。
    User *use_back() - 傳回列表中的最後一個元素。

    這些方法是用於訪問 LLVM 中定義-使用信息的介面。如同 LLVM 中所有其他的迭代器,命名慣例遵循 STL 所定義的慣例。

  • Type *getType() const 這個方法會傳回值的類型。

  • bool hasName() const
    std::string getName() const
    void setName(const std::string &Name)

    這一系列的方法用於訪問和賦予 Value 一個名稱,請注意 上述的注意事項

  • void replaceAllUsesWith(Value *V)

    這個方法會遍歷 Value 的使用列表,將當前值的所有 使用者 改為指向「V」。例如,如果您發現一個指令總是產生一個常數值(例如透過常數折疊),您可以像這樣將指令的所有使用替換為常數

    Inst->replaceAllUsesWith(ConstVal);
    

User 類別

#include "llvm/IR/User.h"

標頭檔來源:User.h

doxygen 資訊:User 類別

父類別:Value

User 類別是所有可能引用 Value 的 LLVM 節點的共同基類別。它公開了一個「運算元」列表,這些運算元都是 User 所引用的 ValueUser 類別本身是 Value 的子類別。

User 的運算元直接指向它所引用的 LLVM Value。因為 LLVM 使用靜態單一賦值(SSA)形式,所以只能引用一個定義,允許這種直接連接。這種連接提供了 LLVM 中的使用-定義信息。

User 類別的重要公開成員

User 類別以兩種方式公開運算元列表:透過索引訪問介面和透過基於迭代器的介面。

  • Value *getOperand(unsigned i)
    unsigned getNumOperands()

    這兩個方法以方便直接訪問的形式公開 User 的運算元。

  • User::op_iterator - 運算元列表迭代器的類型定義
    op_iterator op_begin() - 取得運算元列表開頭的迭代器。
    op_iterator op_end() - 取得運算元清單結尾的迭代器。

    這些方法共同構成了基於迭代器的介面,用於訪問 User 的運算元。

Instruction 類別

#include "llvm/IR/Instruction.h"

標頭檔來源:Instruction.h

doxygen 資訊:Instruction 類別

父類別:UserValue

Instruction 類別是所有 LLVM 指令的共同基類別。它只提供了一些方法,但卻是一個非常常用的類別。Instruction 類別本身主要追蹤的數據是操作碼(指令類型)和嵌入 Instruction 的父 BasicBlock。為了表示特定類型的指令,會使用 Instruction 的眾多子類別之一。

由於 Instruction 類別是 User 類別的子類別,因此可以使用與其他 User 相同的方式訪問其運算元(使用 getOperand()/getNumOperands()op_begin()/op_end() 方法)。Instruction 類別的一個重要檔案是 llvm/Instruction.def 檔案。該檔案包含有關 LLVM 中各種不同類型指令的一些中繼資料。它描述了用作操作碼的列舉值(例如 Instruction::AddInstruction::ICmp),以及實現指令的 Instruction 的具體子類別(例如 BinaryOperatorCmpInst)。遺憾的是,該檔案中宏的使用會讓 doxygen 感到困惑,因此這些列舉值無法在 doxygen 輸出 中正確顯示。

Instruction 類別的重要子類別

  • BinaryOperator

    此子類別表示所有兩個運算元必須屬於相同類型的雙運算元指令,比較指令除外。

  • CastInst 此子類別是 12 個強制轉換指令的父類別。它提供了強制轉換指令的通用操作。

  • CmpInst

    此子類別表示兩個比較指令,ICmpInst(整數運算元)和 FCmpInst(浮點數運算元)。

Instruction 類別的重要公開成員

  • BasicBlock *getParent()

    返回嵌入此 指令基本區塊

  • 布林值 mayWriteToMemory()

    如果指令寫入記憶體,則返回 true,即它是 呼叫釋放調用儲存

  • 無符號 getOpcode()

    返回 指令 的操作碼。

  • 指令 *clone() 常數

    返回指定指令的另一個實例,在所有方面都與原始指令相同,只是指令沒有父項(即它沒有嵌入到 基本區塊 中),並且它沒有名稱。

常數 類別和子類別

常數表示不同類型常數的基類。它被 ConstantInt、ConstantArray 等子類化,用於表示各種類型的常數。全域值也是一個子類別,它表示全域變數或函數的地址。

常數的重要子類別

  • ConstantInt:常數的這個子類別表示任意寬度的整數常數。

    • 常數 APInt& getValue() 常數:返回此常數的底層值,一個 APInt 值。

    • int64_t getSExtValue() 常數:通過符號擴展將底層 APInt 值轉換為 int64_t。如果 APInt 的值(不是位寬)太大而無法放入 int64_t 中,則會導致斷言。因此,不鼓勵使用此方法。

    • uint64_t getZExtValue() 常數:通過零擴展將底層 APInt 值轉換為 uint64_t。如果 APInt 的值(不是位寬)太大而無法放入 uint64_t 中,則會導致斷言。因此,不鼓勵使用此方法。

    • 靜態 ConstantInt* get(常數 APInt& Val):返回表示 Val 提供的值的 ConstantInt 對象。類型隱含為與 Val 的位寬相對應的 IntegerType。

    • 靜態 ConstantInt* get(常數 類型 *Ty, uint64_t Val):返回表示整數類型 TyVal 提供的值的 ConstantInt 對象。

  • ConstantFP:此類別表示浮點常數。

    • 雙精度 getValue() 常數:返回此常數的底層值。

  • ConstantArray:這表示一個常數陣列。

    • 常數 std::vector<使用> &getValues() 常數:返回構成此陣列的組件常數的向量。

  • ConstantStruct:這表示一個常數結構。

    • 常數 std::vector<使用> &getValues() 常數:返回構成此陣列的組件常數的向量。

  • 全域值:這表示一個全域變數或一個函式。不論是哪一種情況,其值都是一個固定的常數地址(在連結之後)。

GlobalValue 類別

#include "llvm/IR/GlobalValue.h"

標頭檔來源:GlobalValue.h

doxygen 資訊:GlobalValue 類別

父類別:常數使用者

全域值(全域變數函式)是 LLVM 中唯一在所有函式主體中可見的值。由於它們在全域範圍內可見,因此它們也需要與其他翻譯單元中定義的其他全域變數進行連結。為了控制連結過程,GlobalValue 知道它們的連結規則。具體來說,GlobalValue 知道它們是具有內部連結還是外部連結,如 LinkageTypes 列舉類型所定義。

如果 GlobalValue 具有內部連結(相當於 C 語言中的 static),則它對當前翻譯單元之外的程式碼不可見,並且不參與連結。如果它具有外部連結,則它對外部程式碼可見,並且參與連結。除了連結資訊之外,GlobalValue 還會追蹤它們當前屬於哪個模組

由於 GlobalValue 是記憶體物件,因此始終透過其**地址**來引用它們。因此,全域變數的類型始終是指向其內容的指標。在使用 GetElementPtrInst 指令時,請務必記住這一點,因為必須先將此指標解引用。例如,如果您有一個類型為 [24 x i32]GlobalVariableGlobalValue 的子類別),則該 GlobalVariable 是指向該陣列的指標。儘管該陣列第一個元素的地址和 GlobalVariable 的值相同,但它們的類型不同。GlobalVariable 的類型為 [24 x i32]。第一個元素的類型為 i32。因此,訪問全域變數需要您先使用 GetElementPtrInst 解引用指標,然後才能訪問其元素。這在LLVM 語言參考手冊中有詳細說明。

GlobalValue 類別的重要公開成員

  • bool hasInternalLinkage() const
    bool hasExternalLinkage() const
    void setInternalLinkage(bool HasInternalLinkage)

    這些方法操作 GlobalValue 的連結特性。

  • Module *getParent()

    這會返回當前嵌入 模組 的 GlobalValue。

Function 類別

#include "llvm/IR/Function.h"

標頭來源:Function.h

doxygen 資訊:Function 類別

超類別:GlobalValueConstantUserValue

Function 類別表示 LLVM 中的單一程序。它實際上是 LLVM 階層中較複雜的類別之一,因為它必須追蹤大量數據。Function 類別會追蹤 基本區塊 清單、形式 引數 清單和 符號表

基本區塊 清單是 Function 物件中最常使用的部分。該清單對函數中的區塊強加了隱式排序,這表明後端將如何佈局程式碼。此外,第一個 基本區塊Function 的隱式入口節點。在 LLVM 中,明確分支到這個初始區塊是不合法的。沒有隱式出口節點,實際上單個 Function 可能有多個出口節點。如果 基本區塊 清單為空,則表示 Function 實際上是一個函數聲明:函數的實際主體尚未連結進來。

除了 基本區塊 清單之外,Function 類別還會追蹤函數接收的形式 引數 清單。這個容器管理 引數 節點的生命週期,就像 基本區塊 清單管理 基本區塊 一樣。

符號表 是一個很少使用的 LLVM 功能,僅在您必須按名稱查找值時使用。除此之外,符號表 在內部用於確保函數主體中的 指令基本區塊引數 的名稱之間沒有衝突。

請注意,Function 是一個 GlobalValue,因此也是一個 Constant。函數的值是它的地址(連結後),保證是常數。

Function 的重要公開成員

  • Function(const FunctionType *Ty, LinkageTypes Linkage, const std::string &N = "", Module* Parent = 0)

    當您需要建立新的 Function 來加入程式時,會使用此建構函式。建構函式必須指定要建立的函式類型以及函式應該具有的連結類型。FunctionType 引數指定函式的形式引數和回傳值。相同的 FunctionType 值可以用於建立多個函式。Parent 引數指定定義函式的模組。如果提供了此引數,函式將自動插入到該模組的函式清單中。

  • bool isDeclaration()

    回傳 Function 是否定義了主體。如果函式是「外部的」,它就沒有主體,因此必須透過與不同翻譯單元中定義的函式連結來解析。

  • Function::iterator - 基本區塊清單迭代器的 Typedef
    Function::const_iterator - const_iterator 的 Typedef。
    begin()end()size()empty()insert()splice()erase()

    這些是轉發方法,可以輕鬆存取 Function 物件的 BasicBlock 清單內容。

  • Function::arg_iterator - 引數清單迭代器的 Typedef
    Function::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()

    這會遍歷 FunctionType 並回傳函式的回傳類型,或實際函式的 FunctionType

  • SymbolTable *getSymbolTable()

    回傳指向此 FunctionSymbolTable 的指標。

GlobalVariable 類別

#include "llvm/IR/GlobalVariable.h"

標頭檔來源:GlobalVariable.h

doxygen 資訊:GlobalVariable Class

超類別:GlobalValueConstantUserValue

全域變數以 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 參數指定變數的連結類型(內部、外部、弱、連結一次、附加)。如果連結為 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 Class

父類別:Value

這個類別表示程式碼中單一進入點和單一離開點的區段,在編譯器領域通常稱為基本區塊。 BasicBlock 類別維護一個 指令 列表,這些指令構成了區塊的主體。根據語言定義,這個指令列表的最後一個元素始終是一個終止指令。

除了追蹤組成區塊的指令列表之外,BasicBlock 類別還會追蹤它嵌入到的 函式

請注意,BasicBlock 本身也是 ,因為它們被分支等指令所引用,並且可以進入跳轉表。 BasicBlock 的類型為 label

BasicBlock 類別的重要公開成員

  • BasicBlock(const std::string &Name = "", Function *Parent = 0)

    BasicBlock 建構函式用於建立新的基本區塊,以便插入到函式中。建構函式可以選擇性地接受新區塊的名稱,以及要插入到的 函式。如果指定了 Parent 參數,則新的 BasicBlock 會自動插入到指定的 函式 的末尾,如果未指定,則必須手動將 BasicBlock 插入到 函式 中。

  • BasicBlock::iterator - 指令列表迭代器的類型定義
    BasicBlock::const_iterator - const_iterator 的類型定義。
    begin()end()front()back()size()empty()splice() - 用於訪問指令列表的 STL 風格函式。

    這些方法和類型定義是轉發函式,它們的語義與同名標準程式庫方法相同。這些方法以易於操作的方式公開了基本區塊的底層指令列表。

  • Function *getParent()

    傳回指向區塊嵌入到的 函式 的指標,如果區塊是無宿主狀態,則傳回空指標。

  • Instruction *getTerminator()

    傳回指向出現在 BasicBlock 末尾的終止指令的指標。如果沒有終止指令,或者區塊中的最後一條指令不是終止指令,則傳回空指標。

Argument 類別

這個 Value 的子類別定義了函式傳入形式參數的介面。一個函式會維護其形式參數的列表。一個參數擁有一個指向父函式的指標。