LLVM 程序员手册

警告

这始终是一项正在进行中的工作。

简介

本文档旨在重点介绍 LLVM 源代码库中提供的一些重要类和接口。本手册并非旨在解释 LLVM 是什么、它是如何工作的以及 LLVM 代码是什么样的。它假定您了解 LLVM 的基础知识,并且有兴趣编写转换或以其他方式分析或操作代码。

本文档应该让您了解方向,以便您可以在不断增长的构成 LLVM 基础设施的源代码中找到方向。请注意,本手册并非旨在替代阅读源代码,因此如果您认为这些类中的某个类应该有一个方法来做某事,但它没有列出,请查看源代码。提供了到 doxygen 源代码的链接,以使此操作尽可能容易。

本文档的第一部分描述了在 LLVM 基础设施中工作时需要了解的通用信息,第二部分描述了核心 LLVM 类。将来,本手册将扩展信息,描述如何使用扩展库,例如支配信息、CFG 遍历例程以及有用的实用程序,例如 InstVisitor (doxygen) 模板。

通用信息

本节包含通用信息,如果您在 LLVM 源代码库中工作,这些信息很有用,但并非特定于任何特定的 API。

C++ 标准模板库

LLVM 大量使用 C++ 标准模板库 (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 版,第 2 卷。(更好的是,买这本书).

还鼓励您查看 LLVM 编码标准 指南,该指南侧重于如何编写可维护的代码,而不是将花括号放在哪里。

其他有用的参考资料

  1. 跨平台使用静态和共享库

重要且有用的 LLVM API

在这里,我们重点介绍一些通常有用且在编写转换时值得了解的 LLVM 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<> 运算符是 “checked 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<> 运算符是 “checking 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

传递字符串(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.h (doxygen)。

您应该很少直接使用 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> 的模板特化,并带有适当的静态 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);
  }
}

这是一种有用的可扩展性机制,用于添加对使用您自己的自定义 Style 选项格式化您自己的自定义类型的支持。但是,当您想要扩展用于格式化库已经知道如何格式化的类型时,它没有帮助。为此,我们需要其他东西。

  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 契约的行为,代表程序本身内部的错误。我们的目标是记录不变式,并在运行时违反不变式时在故障点快速中止(提供一些基本诊断)。

用于处理程序性错误的基本工具是断言和 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> 实用程序。此类型的值可以使用 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 函数还会返回一个必须被检查或传播的 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)。错误类型的感染性意味着尝试将这些函数之一更改为返回 ErrorExpected<T> 通常会导致调用者、调用者的调用者等等发生雪崩式的更改。(第一次这样的尝试,从 MachOObjectFile 的构造函数返回 Error,在 diff 达到 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,可以选择在前面加上一个字符串“banner”,可以通过调用 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 传递到实际的构造函数中,构造函数然后可以使用该 Error 返回错误。ErrorAsOutParameter 实用程序在进入构造函数时设置 Error 值的 checked 标志,以便可以分配错误,然后在退出时重置它,以强制客户端(命名构造函数)检查错误。

通过使用这种惯用法,尝试构造 Foo 的客户端要么接收到一个格式良好的 Foo,要么接收到一个 Error,永远不会接收到一个处于无效状态的对象。

基于类型传播和消费错误

在某些上下文中,已知某些类型的错误是良性的。例如,在遍历归档文件时,一些客户端可能很乐意跳过格式错误的 object 文件,而不是立即终止遍历。跳过格式错误的对象可以使用精心设计的处理程序方法来实现,但 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

构建易错的迭代器和迭代器范围

上面的归档文件遍历示例按索引检索归档文件成员,但是这需要大量的样板代码用于迭代和错误检查。我们可以通过使用“易错的迭代器”模式来清理它,该模式支持以下用于易错容器(如 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_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 (doxygen) 类模板表示对可调用对象的引用,该模板在可调用对象的类型上进行模板化。如果您不需要在函数返回后保留回调,那么这是将回调传递给函数的一个不错的选择。这样,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 时,您会将一堆调试打印输出和其他代码放入您的 pass 中。在您使其工作后,您想要删除它,但将来您可能再次需要它(以解决您遇到的新 bug)。

自然地,因此,您不想删除调试打印输出,但您不希望它们总是很嘈杂。一个标准的折衷方案是将它们注释掉,允许您在将来需要它们时启用它们。

llvm/Support/Debug.h (doxygen) 文件提供了一个名为 LLVM_DEBUG() 的宏,它是解决此问题的更友好的解决方案。基本上,您可以将任意代码放入 LLVM_DEBUG 宏的参数中,并且只有在 ‘opt’(或任何其他工具)与 ‘-debug’ 命令行参数一起运行时,它才会被执行。

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 或尝试跟踪 bug 时,能够控制您的 pass 中的某些事情是否发生会很有用。例如,有时最小化工具只能轻松地为您提供大型测试用例。您希望使用二分法自动将您的 bug 缩小到特定转换是否发生。

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。构建一个 reproducer 脚本,例如:

#! /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,以及 指令选择 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 中,如果您 call DAG.setGraphColor(node, "color"),那么下一次 call DAG.viewGraph() 调用将以指定的颜色突出显示该节点(颜色选择可以在 colors 中找到)。更复杂的节点属性可以使用 call DAG.setGraphAttrs(node, "attributes") 提供(选择可以在 Graph attributes 中找到)。如果您想重启并清除所有当前的图形属性,那么您可以 call DAG.clearGraphAttrs()

请注意,图形可视化功能在 Release 构建版本中被编译移除,以减小文件大小。这意味着您需要 Debug+Asserts 或 Release+Asserts 构建版本才能使用这些功能。

为任务选择正确的数据结构

LLVM 在 llvm/ADT/ 目录中拥有大量数据结构,并且我们通常使用 STL 数据结构。本节描述了您在选择数据结构时应考虑的权衡。

第一步是选择您自己的冒险:您想要顺序容器、类似集合的容器还是类似映射的容器?选择容器时最重要的是您计划如何访问容器的算法属性。基于此,您应该使用

  • 如果您需要基于另一个值高效查找值,则使用类似映射的容器。类似映射的容器也支持高效的包含性查询(键是否在映射中)。类似映射的容器通常不支持高效的反向映射(值到键)。如果您需要这样做,请使用两个映射。一些类似映射的容器还支持按排序顺序高效地迭代键。类似映射的容器是最昂贵的类型,仅在您需要这些功能之一时才使用它们。

  • 如果您需要将一堆东西放入一个自动消除重复项的容器中,则使用类似集合的容器。一些类似集合的容器支持按排序顺序高效地迭代元素。类似集合的容器比顺序容器更昂贵。

  • 如果您需要以最有效的方式添加元素并跟踪它们添加到集合的顺序,则使用顺序容器。它们允许重复项并支持高效迭代,但不支持基于键的高效查找。

  • 如果您需要用于字符或字节数组的专用顺序容器或引用结构,则使用字符串容器。

  • 如果您需要一种高效的方式来存储和对数字 ID 集合执行集合操作,同时自动消除重复项,则使用容器。位容器对于您要存储的每个标识符最多需要 1 位。

一旦确定了容器的正确类别,您就可以通过明智地选择该类别的成员来微调内存使用、常数因子和访问的缓存行为。请注意,常数因子和缓存行为可能非常重要。例如,如果您有一个通常只包含少量元素(但可能包含很多元素)的向量,那么最好使用 SmallVector 而不是 vector。这样做可以避免(相对)昂贵的 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(这样做会浪费大量空间)。因此,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. 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[] 访问页面的第一个元素时,它会分配 PageSizeType 类型的元素。这对于预先知道元素数量的情况很有用;它们的实际初始化成本很高;并且它们很少使用。此实用程序在访问元素时使用页面粒度的延迟初始化。当使用的页面数量较少时,可以实现显着的内存节省。

主要优点是 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::vectorSmallVector 不同)。此外,std::list 的迭代器失效特性比向量类更强:在列表中插入或删除元素不会使列表中其他元素的迭代器或指针失效。

llvm/ADT/ilist.h

ilist<T> 实现了一个“侵入式”双向链表。它是侵入式的,因为它要求元素存储并提供对列表的 prev/next 指针的访问。

ilist 具有与 std::list 相同的缺点,并且还需要元素类型的 ilist_traits 实现,但它提供了一些新颖的特性。特别是,它可以高效地存储多态对象,当元素插入或从列表中删除时,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> 公开从此 traits 类派生。

llvm/ADT/ilist_node.h

ilist_node<T> 以默认方式实现 ilist<T>(和类似容器)期望的前向和后向链接。

ilist_node<T> 旨在嵌入到节点类型 T 中,通常 T 公开从 ilist_node<T> 派生。

哨兵

ilist 还有另一个必须考虑的特殊性。为了成为 C++ 生态系统中的好公民,它需要支持标准容器操作,例如 beginend 迭代器等。此外,在非空 ilist 的情况下,operator-- 必须在 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 的主要缺点是它们的大小。虽然它们针对小字符串进行了优化,但它们本身并不是特别小。这意味着它们非常适合堆栈上的临时暂存缓冲区,但不应通常放入堆中:很少看到 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” 方法。客户端拥有关于想要生成什么的描述(它知道操作码和所有操作数),但我们不想 ‘new’ 一个节点,然后尝试将其插入到一个集合中,结果却发现它已经存在,这时我们将不得不删除它并返回已经存在的节点。

为了支持这种风格的客户端,FoldingSet 使用 FoldingSetNodeID(它包装了 SmallVector)执行查询,FoldingSetNodeID 可用于描述我们想要查询的元素。查询要么返回与 ID 匹配的元素,要么返回一个指示应插入位置的不透明 ID。ID 的构造通常不需要堆流量。

由于 FoldingSet 使用侵入式链接,它可以支持集合中的多态对象(例如,你可以混合使用 SDNode 实例和 LoadSDNodes)。由于元素是单独分配的,因此指向元素的指针是稳定的:插入或删除元素不会使指向其他元素的任何指针失效。

<set>

std::set 是一个合理的通用集合类,它在许多方面都表现尚可,但在任何方面都不出色。std::set 为每个插入的元素分配内存(因此它是内存分配密集型的),并且通常在集合中为每个元素存储三个指针(因此增加了大量的每元素空间开销)。它提供保证的 log(n) 性能,从复杂性的角度来看,这并不是特别快(特别是如果集合的元素比较昂贵,例如字符串),并且在查找、插入和删除方面具有极高的常数因子。

std::set 的优点是其迭代器是稳定的(从集合中删除或插入元素不会影响迭代器或指向其他元素的指针),并且对集合的迭代保证按排序顺序进行。如果集合中的元素很大,那么指针和内存分配流量的相对开销并不算什么,但如果集合的元素很小,那么 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 两者的成本之和,它具有高复杂性、高常数因子,并产生大量的内存分配流量。应该避免使用它。

llvm/ADT/ImmutableSet.h

ImmutableSet 是一个基于 AVL 树的不可变(函数式)集合实现。添加或删除元素通过 Factory 对象完成,并导致创建新的 ImmutableSet 对象。如果具有给定内容的 ImmutableSet 已经存在,则返回现有的 ImmutableSet;相等性与 FoldingSetNodeID 进行比较。添加或删除操作的时间和空间复杂度与原始集合的大小成对数关系。

没有返回集合元素的方法,你只能检查成员资格。

其他类似集合的容器选项

STL 提供了其他几个选项,例如 std::multiset 和 std::unordered_set。我们从不使用像 unordered_set 这样的容器,因为它们通常非常昂贵(每次插入都需要内存分配)。

如果你不关心消除重复项,std::multiset 很有用,但它具有 std::set 的所有缺点。排序向量(你不删除重复条目的地方)或其他一些方法几乎总是更好。

类似 Map 的容器(std::map、DenseMap 等)

当你想要将数据与键关联时,类似 Map 的容器很有用。像往常一样,有很多不同的方法可以做到这一点。 :)

排序的 ‘vector’

如果你的使用模式遵循严格的插入然后查询方法,你可以轻松地使用与 用于类似集合的容器的排序向量 相同的方法。唯一的区别是你的查询函数(它使用 std::lower_bound 来获得高效的 log(n) 查找)应该只比较键,而不是键和值。这产生了与用于集合的排序向量相同的优点。

llvm/ADT/StringMap.h

字符串通常用作 map 中的键,并且它们很难有效地支持:它们是可变长度的,在长字符串时散列和比较效率低下,复制成本高昂等等。StringMap 是一个专门设计的容器,用于应对这些问题。它支持将任意字节范围映射到任意其他对象。

StringMap 实现使用二次探测哈希表,其中桶存储指向堆分配条目的指针(以及其他一些东西)。map 中的条目必须是堆分配的,因为字符串是可变长度的。字符串数据(键)和元素对象(值)存储在同一个分配中,字符串数据紧跟在元素对象之后。此容器保证 “(char*)(&Value+1)” 指向值的键字符串。

StringMap 非常快,原因有几个:二次探测对于查找非常缓存友好,桶中字符串的哈希值在查找元素时不会重新计算,StringMap 在查找值时很少需要访问不相关对象的内存(即使发生哈希冲突时也是如此),哈希表增长不会重新计算表中已存在字符串的哈希值,并且 map 中的每对都存储在单个分配中(字符串数据与对的值存储在同一个分配中)。

StringMap 还提供了接受字节范围的查询方法,因此只有当值插入到表中时,它才会复制字符串。

但是,StringMap 迭代顺序不保证是确定性的,因此任何需要确定性的用法都应改为使用 std::map。

llvm/ADT/IndexedMap.h

IndexedMap 是一个专门的容器,用于将小的密集整数(或可以映射到小的密集整数的值)映射到其他类型。它在内部实现为一个向量,带有一个映射函数,该函数将键映射到密集整数范围。

这对于像 LLVM 代码生成器中的虚拟寄存器这样的情况很有用:它们具有由编译时常量(第一个虚拟寄存器 ID)偏移的密集映射。

llvm/ADT/DenseMap.h

DenseMap 是一个简单的二次探测哈希表。它在支持小键和小值方面表现出色:它使用单个分配来保存当前插入到 map 中的所有对。DenseMap 是将指针映射到指针,或将其他小类型相互映射的绝佳方法。

但是,你应该注意 DenseMap 的几个方面。DenseMap 中的迭代器在每次插入发生时都会失效,这与 map 不同。此外,由于 DenseMap 为大量的键/值对分配空间(默认情况下从 64 开始),如果你的键或值很大,它将浪费大量空间。最后,对于你想要的键,如果它尚未支持,你必须实现 DenseMapInfo 的部分特化。这是必需的,以告诉 DenseMap 关于它在内部需要的两个特殊的标记值(永远不能插入到 map 中)。

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 是用于小键和小值的紧凑 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 的非确定性迭代的简单(但有点昂贵)的解决方案。

它通过从键映射到键值对向量中的索引来实现。这提供了快速查找和迭代,但有两个主要缺点:键存储两次,并且删除元素需要线性时间。如果必须删除元素,最好使用 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 这样的容器,因为它们通常非常昂贵(每次插入都需要内存分配)。

如果你想将键映射到多个值,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(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";

请注意,元素通过 ‘引用包装器’ 代理类型(引用元组)提供,这与结构化绑定声明相结合,使 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

长度要求使用 asserts 检查。

作为经验法则,当你期望所有范围都具有相同的长度时,首选使用 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 美化打印器。要使用它们,请执行以下操作(或将其添加到您的 ~/.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::iteratorjBasicBlock::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 的所有指令就像迭代 Fdef-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) 的实例,需要知道它使用了哪些 ValueUser 使用的所有 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需要一个(指向 const 的指针)Type。 因此

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

将创建一个 AllocaInst 实例,该实例表示在运行时在当前堆栈帧中分配一个整数。 每个 Instruction 子类都可能具有不同的默认参数,这些参数会更改指令的语义,因此请参阅您感兴趣实例化的 Instruction 子类的 doxygen 文档

命名值

当您能够命名指令的值时,它非常有用,因为这有助于调试您的转换。 如果您最终查看生成的 LLVM 机器代码,您肯定希望将逻辑名称与指令执行的结果相关联! 通过为 Instruction 构造函数的 Name(默认)参数提供一个值,您可以将逻辑名称与指令在运行时执行的结果相关联。 例如,假设我正在编写一个转换,该转换在堆栈上动态分配一个整数空间,并且该整数将用作某些其他代码的某种索引。 为了实现这一点,我将一个 AllocaInst 放置在某个 Function 的第一个 BasicBlock 中的第一个点,并且我打算在同一个 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 之前插入了三个指令。 前两个指令是 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 教程

删除指令

从构成 BasicBlock 的现有指令序列中删除指令非常简单:只需调用指令的 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

    AllocaInst* instToReplace = ...;
    BasicBlock::iterator ii(instToReplace);
    
    ReplaceInstWithInst(instToReplace->getParent(), ii,
                        new AllocaInst(Type::Int32Ty, 0, "ptrToReplacedInt"));
    
替换 Users 和 Values 的多个用途

您可以使用 Value::replaceAllUsesWithUser::replaceUsesOfWith 一次更改多个用途。 有关更多信息,请参阅 Value 类User 类 的 doxygen 文档。

删除 GlobalVariables

从模块中删除全局变量就像删除指令一样容易。 首先,您必须有一个指向您想要删除的全局变量的指针。 您可以使用此指针从其父模块中擦除它。 例如

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 内存 IR 中的每个 LLVM 实体(ModuleValueTypeConstant 等)都属于一个 LLVMContext。 不同上下文中的实体不能相互交互:不同上下文中的 Module 不能链接在一起,Function 不能添加到不同上下文中的 Module 等。 这意味着在多个线程上同时编译是安全的,只要没有两个线程在同一上下文中操作实体。

实际上,API 中很少有地方需要显式指定 LLVMContext,除了 Type 创建/查找 API 之外。 因为每个 Type 都带有对其所属上下文的引用,所以大多数其他实体都可以通过查看自己的 Type 来确定它们属于哪个上下文。 如果您要向 LLVM IR 添加新实体,请尽量保持此接口设计。

线程和 JIT

LLVM 的“eager” JIT 编译器可以安全地在线程程序中使用。 多个线程可以并发调用 ExecutionEngine::getPointerToFunction()ExecutionEngine::runFunction(),并且多个线程可以并发运行 JIT 输出的代码。 用户仍然必须确保只有一个线程可以访问给定 LLVMContext 中的 IR,而另一个线程可能正在修改它。 一种方法是在 JIT 外部访问 IR 时始终持有 JIT 锁(JIT 通过添加 CallbackVH修改 IR)。 另一种方法是仅从 LLVMContext 的线程调用 getPointerToFunction()

当 JIT 配置为延迟编译时(使用 ExecutionEngine::DisableLazyCompilation(false)),在延迟 JIT 函数后更新调用站点时,目前存在一个 竞争条件。 如果您确保一次只有一个线程可以调用任何特定的延迟桩,并且 JIT 锁保护任何 IR 访问,则仍然可以在线程程序中使用延迟 JIT,但我们建议在线程程序中仅使用 eager JIT。

高级主题

本节介绍大多数客户端不需要注意的一些高级或晦涩的 API。 这些 API 倾向于管理 LLVM 系统的内部工作原理,并且仅在不寻常的情况下才需要访问。

ValueSymbolTable

ValueSymbolTable (doxygen) 类提供了一个符号表,FunctionModule 类使用该符号表来命名值定义。 符号表可以为任何 Value 提供名称。

请注意,大多数客户端不应直接访问 SymbolTable 类。 它仅应在需要迭代符号表名称本身时使用,这是一种非常特殊的用途。 请注意,并非所有 LLVM Value 都有名称,并且那些没有名称的(即,它们具有空名称)不存在于符号表中。

符号表支持使用 begin/end/iterator 迭代符号表中的值,并支持查询以查看特定名称是否在符号表中(使用 lookup)。 ValueSymbolTable 类不公开公共 mutator 方法,而是简单地在值上调用 setName,这将自动将其插入到适当的符号表中。

User 和拥有的 Use 类的内存布局

User (doxygen) 类为表达 User 对其他 Value 实例 的所有权提供了基础。 Use (doxygen) 辅助类用于进行簿记并促进 O(1) 添加和删除。

UserUse 对象之间的交互和关系

User 的子类可以选择合并其 Use 对象,或者通过指针以 out-of-line 方式引用它们。 混合变体(一些 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 的论文和演示文稿 - 链接到幻灯片、视频,有时还有代码。

在创建类型层次结构(使用标记分发或虚函数分发)与使用模板或基于概念的多态性之间进行选择时,请考虑抽象基类的细化是否是接口边界上语义上有意义的类型。如果任何比根抽象接口更精细的东西作为语义模型的局部扩展而言都是毫无意义的,那么您的用例可能更适合多态性,并且您应该避免使用虚函数分发。但是,可能存在一些紧急情况需要使用其中一种技术。

如果您确实需要引入类型层次结构,我们倾向于使用显式封闭的类型层次结构,并使用手动标记分发和/或 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* 值,如果指针相同,则类型是相同的。

重要的公共方法

  • 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 - 函数列表迭代器的类型定义
    Module::const_iterator - const_iterator 的类型定义。
    begin()end()size()empty()

    这些是转发方法,可以轻松访问 Module 对象的 Function 列表的内容。

  • Module::FunctionListType &getFunctionList()

    返回 Function 的列表。当您需要更新列表或执行没有转发方法的复杂操作时,这是必要的。


  • Module::global_iterator - 全局变量列表迭代器的类型定义
    Module::const_global_iterator - const_iterator 的类型定义。
    Module::insertGlobalVariable() - 将全局变量插入列表。
    Module::removeGlobalVariable() - 从列表中删除全局变量。
    Module::eraseGlobalVariable() - 从列表中删除全局变量并删除它。
    global_begin()global_end()global_size()global_empty()

    这些是转发方法,可以轻松访问 Module 对象的 GlobalVariable 列表的内容。


  • SymbolTable *getSymbolTable()

    返回对此 ModuleSymbolTable 的引用。


  • 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,例如 ConstantArgument。甚至 InstructionFunction 也是 Value

一个特定的 Value 可以在 LLVM 程序表示中使用多次。例如,函数的传入参数(用 Argument 类的实例表示)被函数中引用该参数的每个指令“使用”。为了跟踪这种关系,Value 类保留了正在使用它的所有 User 的列表(User 类是 LLVM 图中所有可以引用 Value 的节点的基类)。此使用列表是 LLVM 如何在程序中表示 def-use 信息的方式,并且可以通过下面显示的 use_* 方法访问。

由于 LLVM 是类型化的表示,因此每个 LLVM Value 都是类型化的,并且可以通过 getType() 方法获得此 Type。此外,所有 LLVM 值都可以命名。Value 的“名称”是在 LLVM 代码中打印的符号字符串

%foo = add i32 1, 2

此指令的名称是“foo”。注意,任何值的名称都可能缺失(空字符串),因此名称应用于调试(使源代码更易于阅读,调试输出),不应用于跟踪值或在它们之间进行映射。为此目的,请使用指向 Value 本身的指针的 std::map

LLVM 的一个重要方面是 SSA 变量与其生成的操作之间没有区别。因此,对指令生成的值(或例如作为传入参数可用的值)的任何引用都表示为指向表示此值的类实例的直接指针。虽然这可能需要一些时间来适应,但它简化了表示并使其更易于操作。

Value 类的重要的公共成员

  • Value::use_iterator - use-list 上的迭代器的类型定义
    Value::const_use_iterator - use-list 上的 const_iterator 的类型定义
    unsigned use_size() - 返回 value 的使用者数量。
    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 的类型。

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

    这组方法用于访问和分配 Value 的名称,请注意上面的 预防措施

  • void replaceAllUsesWith(Value *V)

    此方法遍历 Value 的 use 列表,将当前值的所有 User 更改为引用 “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 中的 use-def 信息。

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()

    返回此 Instruction 嵌入到的 BasicBlock

  • bool mayWriteToMemory()

    如果指令写入内存,则返回 true,即它是 callfreeinvokestore

  • 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):返回表示整数类型 TyVal 值所提供的 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 类

超类: ConstantUserValue

全局值(GlobalVariableFunction)是唯一在所有 Function 的主体中可见的 LLVM 值。由于它们在全局范围内可见,因此它们也受与不同翻译单元中定义的其他全局变量链接的约束。为了控制链接过程,GlobalValue 知道它们的链接规则。具体来说,GlobalValue 知道它们是否具有内部或外部链接,由 LinkageTypes 枚举定义。

如果 GlobalValue 具有内部链接(等效于 C 中的 static),则它对当前翻译单元之外的代码不可见,并且不参与链接。如果它具有外部链接,则它对外部代码可见,并且参与链接。除了链接信息外,GlobalValue 还跟踪它们当前所属的 Module

由于 GlobalValue 是内存对象,因此它们始终通过其 地址 引用。因此,全局变量的 Type 始终是指向其内容的指针。在使用 GetElementPtrInst 指令时,记住这一点非常重要,因为必须首先解引用此指针。例如,如果您有一个 GlobalVariableGlobalValue 的子类),它是一个包含 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 类

超类: GlobalValueConstantUserValue

Function 类表示 LLVM 中的单个过程。它实际上是 LLVM 层级结构中更复杂的类之一,因为它必须跟踪大量数据。Function 类跟踪 BasicBlock 列表、形式 Argument 列表和 SymbolTable

BasicBlock 列表是 Function 对象最常用的部分。该列表对函数中的块强制执行隐式排序,这表明后端将如何布局代码。此外,第一个 BasicBlockFunction 的隐式入口节点。在 LLVM 中,显式分支到此初始块是不合法的。没有隐式出口节点,实际上,单个 Function 可能有多个出口节点。如果 BasicBlock 列表为空,则表示 Function 实际上是函数声明:函数的实际主体尚未链接到其中。

除了 BasicBlock 列表之外,Function 类还跟踪函数接收的形式 Argument 列表。此容器管理 Argument 节点的生命周期,就像 BasicBlock 列表对 BasicBlock 执行的操作一样。

SymbolTable 是一个非常少用的 LLVM 功能,仅当您必须按名称查找值时才使用。除此之外,SymbolTable 在内部用于确保函数体中 InstructionBasicBlockArgument 的名称之间没有冲突。

请注意,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 - 基本块列表迭代器的类型定义
    Function::const_iterator - const_iterator 的类型定义。
    begin()end()size()empty()insert()splice()erase()

    这些是转发方法,使您可以轻松访问 Function 对象的 BasicBlock 列表的内容。

  • Function::arg_iterator - 参数列表迭代器的类型定义
    Function::const_arg_iterator - const_iterator 的类型定义。
    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 类

超类: 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 参数指定变量的链接类型(内部、外部、弱、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 类的重要的公共成员

Argument

Value 的这个子类定义了函数传入的形式参数的接口。Function 维护其形式参数的列表。Argument 具有指向父 Function 的指针。