llvm程序手册

原文地址

注:单纯机翻

  • 1 Introduction
  • 2 General Information
    • 2.1 The C++ Standard Template Library
    • 2.2 Other useful references
  • 3 Important and useful LLVM APIs
    • 3.1 The isa<>, cast<> and dyn_cast<> templates
    • 3.2 Passing strings (the StringRef and Twine classes)
      • 3.2.1 The StringRef class
      • 3.2.2 The Twine class
    • 3.3 Formatting strings (the formatv function)
      • 3.3.1 Simple formatting
      • 3.3.2 Custom formatting
      • 3.3.3 formatv Examples
    • 3.4 Error handling
      • 3.4.1 Programmatic Errors
      • 3.4.2 Recoverable Errors
        • 3.4.2.1 StringError
        • 3.4.2.2 Interoperability with std::error_code and ErrorOr
        • 3.4.2.3 Returning Errors from error handlers
        • 3.4.2.4 Using ExitOnError to simplify tool code
        • 3.4.2.5 Using cantFail to simplify safe callsites
        • 3.4.2.6 Fallible constructors
        • 3.4.2.7 Propagating and consuming errors based on types
        • 3.4.2.8 Concatenating Errors with joinErrors
        • 3.4.2.9 Building fallible iterators and iterator ranges
    • 3.5 Passing functions and other callable objects
      • 3.5.1 Function template
      • 3.5.2 The function_ref class template
    • 3.6 The LLVM_DEBUG() macro and -debug option
      • 3.6.1 Fine grained debug info with DEBUG_TYPE and the -debug-only option
    • 3.7 The Statistic class & -stats option
    • 3.8 Adding debug counters to aid in debugging your code
    • 3.9 Viewing graphs while debugging code
  • 4 Picking the Right Data Structure for a Task
    • 4.1 Sequential Containers (std::vector, std::list, etc)
      • 4.1.1 llvm/ADT/ArrayRef.h
      • 4.1.2 Fixed Size Arrays
      • 4.1.3 Heap Allocated Arrays
      • 4.1.4 llvm/ADT/TinyPtrVector.h
      • 4.1.5 llvm/ADT/SmallVector.h
      • 4.1.6 <vector>
      • 4.1.7 <deque>
      • 4.1.8 <list>
      • 4.1.9 llvm/ADT/ilist.h
      • 4.1.10 llvm/ADT/PackedVector.h
      • 4.1.11 ilist_traits
      • 4.1.12 llvm/ADT/ilist_node.h
      • 4.1.13 Sentinels
      • 4.1.14 Other Sequential Container options
    • 4.2 String-like containers
      • 4.2.1 llvm/ADT/StringRef.h
      • 4.2.2 llvm/ADT/Twine.h
      • 4.2.3 llvm/ADT/SmallString.h
      • 4.2.4 std::string
    • 4.3 Set-Like Containers (std::set, SmallSet, SetVector, etc)
      • 4.3.1 A sorted ‘vector’
      • 4.3.2 llvm/ADT/SmallSet.h
      • 4.3.3 llvm/ADT/SmallPtrSet.h
      • 4.3.4 llvm/ADT/StringSet.h
      • 4.3.5 llvm/ADT/DenseSet.h
      • 4.3.6 llvm/ADT/SparseSet.h
      • 4.3.7 llvm/ADT/SparseMultiSet.h
      • 4.3.8 llvm/ADT/FoldingSet.h
      • 4.3.9 <set>
      • 4.3.10 llvm/ADT/SetVector.h
      • 4.3.11 llvm/ADT/UniqueVector.h
      • 4.3.12 llvm/ADT/ImmutableSet.h
      • 4.3.13 Other Set-Like Container Options
    • 4.4 Map-Like Containers (std::map, DenseMap, etc)
      • 4.4.1 A sorted ‘vector’
      • 4.4.2 llvm/ADT/StringMap.h
      • 4.4.3 llvm/ADT/IndexedMap.h
      • 4.4.4 llvm/ADT/DenseMap.h
      • 4.4.5 llvm/IR/ValueMap.h
      • 4.4.6 llvm/ADT/IntervalMap.h
      • 4.4.7 llvm/ADT/IntervalTree.h
      • 4.4.8 <map>
      • 4.4.9 llvm/ADT/MapVector.h
      • 4.4.10 llvm/ADT/IntEqClasses.h
      • 4.4.11 llvm/ADT/ImmutableMap.h
      • 4.4.12 Other Map-Like Container Options
    • 4.5 Bit storage containers
      • 4.5.1 BitVector
      • 4.5.2 SmallBitVector
      • 4.5.3 SparseBitVector
      • 4.5.4 CoalescingBitVector
    • 4.6 Useful Utility Functions
      • 4.6.1 Iterating over ranges
        • 4.6.1.1 The zip* functions
        • 4.6.1.2 enumerate
  • 5 Debugging
  • 6 Helpful Hints for Common Operations
    • 6.1 Basic Inspection and Traversal Routines
      • 6.1.1 Iterating over the BasicBlock in a Function
      • 6.1.2 Iterating over the Instruction in a BasicBlock
      • 6.1.3 Iterating over the Instruction in a Function
      • 6.1.4 Turning an iterator into a class pointer (and vice-versa)
      • 6.1.5 Finding call sites: a slightly more complex example
      • 6.1.6 Iterating over def-use & use-def chains
      • 6.1.7 Iterating over predecessors & successors of blocks
    • 6.2 Making simple changes
      • 6.2.1 Creating and inserting new Instructions
      • 6.2.2 Deleting Instructions
      • 6.2.3 Replacing an Instruction with another Value
        • 6.2.3.1 Replacing individual instructions
        • 6.2.3.2 Deleting Instructions
        • 6.2.3.3 Replacing multiple uses of Users and Values
      • 6.2.4 Deleting GlobalVariables
  • 7 Threads and LLVM
    • 7.1 Ending Execution with llvm_shutdown()
    • 7.2 Lazy Initialization with ManagedStatic
    • 7.3 Achieving Isolation with LLVMContext
    • 7.4 Threads and the JIT
  • 8 Advanced Topics
    • 8.1 The ValueSymbolTable class
    • 8.2 The User and owned Use classes’ memory layout
      • 8.2.1 Interaction and relationship between User and Use objects
    • 8.3 Designing Type Hierarchies and Polymorphic Interfaces
    • 8.4 ABI Breaking Checks
  • 9 The Core LLVM Class Hierarchy Reference
    • 9.1 The Type class and Derived Types
      • 9.1.1 Important Public Methods
      • 9.1.2 Important Derived Types
    • 9.2 The Module class
      • 9.2.1 Important Public Members of the Module class
    • 9.3 The Value class
      • 9.3.1 Important Public Members of the Value class
    • 9.4 The User class
      • 9.4.1 Important Public Members of the User class
    • 9.5 The Instruction class
      • 9.5.1 Important Subclasses of the Instruction class
      • 9.5.2 Important Public Members of the Instruction class
    • 9.6 The Constant class and subclasses
      • 9.6.1 Important Subclasses of Constant
    • 9.7 The GlobalValue class
      • 9.7.1 Important Public Members of the GlobalValue class
    • 9.8 The Function class
      • 9.8.1 Important Public Members of the Function
    • 9.9 The GlobalVariable class
      • 9.9.1 Important Public Members of the GlobalVariable class
    • 9.10 The BasicBlock class
      • 9.10.1 Important Public Members of the BasicBlock class
    • 9.11 The Argument class

1 介绍

重点介绍LLVM基线源码中一些重要的类和接口, 这个用户手册没有解释LLVM是什么怎么工作这些, 我们假设您对LLVM已经有了基本的了解, 并有兴趣去写一个转换 pass 或分析修改源码.

这个文档将引导你找到自己的方式去适应不断增长的LLVM源代码, 读这个文档不代表可以不读源码, 以源码为准. 在 llvm 的 doxygen 页面中你能更快找到与设想不一致时的答案.

第一部分描述了工作中通用有用信息, 第二部分描述了LLVM 核心的类. 未来这个用户手册准备扩展描述怎么使用扩展的库(如dominator), CFG 遍历例程, 和一些有用的通用组件如 InstVisitor(doxygen) 模板

2 一般信息

这个部分包含了当你在LLVM工作时通用的非常有用的信息, 但不是针对特定场景的API.

2.1 C++ 标准模板库

LLVM 源码中大量使用了 C++ 标准模板库(STL), 所以你可能需要了解一些技术与库背景, 这里有一些非常好的讨论 STL 的文章和书籍

3 重要与实用的 LLVM API

介绍几个常用的.

3.1 isa<>, cast<> , dyn_cast<> 模板

LLVM源代码库广泛使用一种自定义的RTTI形式. 这些模板与C++的dynamic_cast<>运算符有许多相似之处, 但它们没有一些缺点(主要源于dynamic_cast<>只适用于具有虚函数表的类). 由于它们经常被使用, 你必须了解它们的功能和工作原理. 所有这些模板都在 llvm/Support/Casting.h 文件中定义(注意你很少直接包含这个文件).

  • isa<>
    • 非常类似 java 的 instanceof, 返回值 true/false 取决于引用或指针是否指向一个已经实例化的类, 下面有例子
  • 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<>
    • 是一个带检查的操作, 它检查操作数是否为指定类型, 如果是, 返回指针(不能转换引用), 如果不是则返回空指针, 所以非常像 C++ 的 dynamic_cast<>, 通常用在一些if语句或控制流语句.
    if (auto *AI = dyn_cast<AllocationInst>(Val)) {
      // ...
    }
    
    注意这个可以被滥用, 比起用一大推 if/then/else, 使用 InstVisitor 类直接调度指令类型会更简洁、更高效
  • isa_and_nonnull<>
    • 与 isa<> 很像, 不过它允许参数为空指针(返回false), 允许你把很多空指针检查语句和并成一条
  • cast_or_null<>
    • 类似 cast<>, 不过它允许参数为空指针(空指针传递), 允许你把很多空指针检查语句和并成一条
  • dyn_cast_or_null<>
    • 类似 dyn_cast<>, 不过它允许参数为空指针(空指针传递), 允许你把很多空指针检查语句和并成一条

这五个模板可以用于任何类, 他们是否有 v 表, 如果要添加对这些模板的支持, 请参阅文档 How to set up LLVM-style RTTI for your class hierarchy

3.2 stringRef and Twine

虽然LLVM通常不进行太多的字符串操作, 但我们确实有几个重要的API需要使用字符串. 两个重要的例子是Value类(用于指令、函数等的名称)和StringMap类, 在LLVM和Clang中广泛使用.

这些都是通用类, 它们需要能够接受 可能包含嵌入的空字符的字符串. 因此, 它们不能简单地接受const char *, 而使用const std::string&则要求客户端执行通常是不必要的 堆分配. 相反, 许多LLVM API使用 StringRefconst Twine& 来高效传递字符串.

3.2.1 StringRef

StringRef 是一种表示对 常量字符串的引用 的数据类型(包括字符数组和长度), 它支持 std::string 上可用的常见操作, 但不需要堆分配.

可以使用C风格的以空字符结尾的字符串、std::string 隐式构造它, 也可以显式使用字符指针和长度构造. 例如, StringMap的find函数声明如下:

iterator find(StringRef Key);

客户端可以使用以下任一方式调用它:

Map.find("foo"); // 查找"foo"
Map.find(std::string("bar")); // 查找"bar"
Map.find(StringRef("\0baz", 4)); // 查找"\0baz"

类似地, 需要返回字符串的API可能会返回一个StringRef实例, 可以直接使用它或使用 str 成员函数将其转换为std::string. 有关更多信息, 请参阅 llvm/ADT/StringRef.h

你应该很少直接使用 StringRef 类, 因为它包含对外部内存的指针, 通常不安全存储类的实例(除非你知道外部存储不会被释放). 在LLVM中, StringRef 足够小且广泛使用, 因此应始终 按值传递.

个人理解, 可以使用 StringRef 代替 const char *

3.2.2 Twine

Twine 类是API接受连接字符串的高效方式. 例如, LLVM中常见的范例是根据另一个指令的名称给一个指令命名, 并添加后缀, 例如:

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

Twine类实际上是一个轻量级的绳子, 它指向临时(栈分配的)对象. Twine可以通过字符串之间的加法运算符(即C字符串、std::string或StringRef)隐式构造. Twine 推迟了实际字符串连接的操作, 直到真正需要时, 才能将其高效地直接渲染到字符数组中. 这避免了在构造字符串连接的临时结果时涉及的不必要的堆分配. 请参阅 llvm/ADT/Twine.h 和此处以获取更多信息.

3.3 formatv 格式化字符串

尽管LLVM不一定会进行大量的字符串操作和解析, 但它确实会进行大量的字符串格式化. 从诊断消息到 llvm 工具的输出(如llvm-readobj), 再到打印详细的反汇编列表和 LLDB 运行时日志, 字符串格式化的需求无处不在.

formatv 在精神上类似于 printf, 但使用了不同的语法, 借鉴了Python和 C# 的许多特性. 与 printf 不同, 它在编译时推断要格式化的类型, 因此不需要像 %d 这样的格式说明符. 这降低了尝试构建可移植格式字符串的心理负担, 特别是对于像 size_t 或指针类型这样的平台特定类型. 与 printfPython 都不同的是, 如果 LLVM 不知道如何格式化该类型, 它还会导致编译失败. 这两个特性确保该函数比传统的格式化方法(如 printf 系列函数)更安全、更简单易用.

3.3.1 简单格式化

调用 formatv 函数涉及一个格式字符串, 其中包含0个或多个替换序列, 后跟一个可变长度的替换值列表. 替换序列的格式为 {N[[,align]:style]}.

N 表示替换值列表中参数的基于0的索引. 请注意, 这意味着可以以任意顺序多次引用相同的参数, 可能带有不同的样式和/或对齐选项.

align是一个可选的字符串, 用于指定要将值格式化到的字段的宽度以及值在字段中的对齐方式. 它由一个可选的对齐样式后跟一个正整数字段宽度组成. 对齐样式可以是字符 -(左对齐)、=(居中对齐)或+(右对齐). 默认值为右对齐.

style 是一个可选的字符串, 由特定于类型的字符组成, 用于控制值的格式化方式. 例如, 要将浮点值格式化为百分比, 可以使用样式选项 P.

3.3.2 自定义格式

有两种方法可以自定义类型的格式化行为.

  • 一种方法是为您的类型 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);
  }
}

这是一种有用的扩展机制, 用于为您自定义的类型添加支持, 并使用自定义的样式选项进行格式化. 但是, 当您想要扩展库已经知道如何格式化的类型的机制时, 它并不能帮助您. 为此, 我们需要其他的方法.

  • 2、提供 format adapter, 从 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 派生出来的, formatv 将调用该参数上的 format 方法, 并传入指定的样式. 这允许您提供任何类型的自定义格式化, 包括那些已经具有内置格式化提供程序的类型.

3.3.3 formatv Examples

以下是一组不完整的示例, 演示了 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"

3.4 错误处理

正确的错误处理帮助我们识别代码中的错误, 并帮助最终用户理解他们在使用工具时发生的错误. 错误可以分为两个广泛的类别:程序错误可恢复错误, 对于处理和报告这两种错误需要采用不同的策略.

3.4.1 程序错误

程序错误是程序不变式或API约定的违反, 代表着程序本身的错误. 我们的目标是记录不变式, 并在运行时不变式被破坏时, 快速中止程序并提供一些基本的诊断信息.

处理程序错误的基本工具是断言(assertions)和 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: /* 处理 Foo */; break;
  case Bar: /* 处理 Bar */; break;
  default:
    llvm_unreachable("X should be Foo or Bar here");
}

3.4.2 可恢复错误

可恢复错误代表程序 环境中的错误, 例如资源故障(缺少文件、网络连接中断等)或格式错误的输入. 应该检测这些错误并将其传递到能够适当处理的程序级别. 处理错误可能只是向用户报告问题, 也可能涉及尝试恢复的操作.

注意:

尽管在整个 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();
}

Error值可以隐式转换为bool类型:错误为true, 成功为false, 从而实现以下惯用法:

Error mayFail();

Error foo() {
  if (auto Err = mayFail())
    return Err;
  // Success! We can proceed.
  ...

对于可能失败但需要返回值的函数, 可以使用 Expected<T> 工具. 此类型的值可以使用 T 或 Error 构造. Expected<T> 值也可以隐式转换为布尔值, 但与Error 的约定相反:成功为 true, 错误为 false. 如果成功, 可以通过解引用运算符访问 T 值. 如果失败, 可以使用 takeError() 方法提取 Error 值. 典型的用法如下:

Expected<FormattedFile> openFormattedFile(StringRef Path) {
  // If badly formatted, return an error.
  if (auto Err = checkFormat(Path))
    return std::move(Err);
  // Otherwise return a FormattedFile instance.
  return FormattedFile(Path);
}

Error processFormattedFile(StringRef Path) {
  // Try to open a formatted file
  if (auto FileOrErr = openFormattedFile(Path)) {
    // On success, grab a reference to the file and continue.
    auto &File = *FileOrErr;
    ...
  } else
    // On error, extract the Error value and return it.
    return FileOrErr.takeError();
}

如果 Expected<T> 的值处于成功模式, 那么 takeError() 方法将返回一个成功值. 利用这一点, 上述函数可以改写为:

Error processFormattedFile(StringRef Path) {
  // Try to open a formatted file
  auto FileOrErr = openFormattedFile(Path);
  if (auto Err = FileOrErr.takeError())
    // On error, extract the Error value and return it.
    return Err;
  // On success, grab a reference to the file and continue.
  auto &File = *FileOrErr;
  ...
}

如果 Expected<T> 的值将被移动到现有变量中, 那么 moveInto() 方法可以避免命名额外的变量. 这对于使 Expected<T> 值具有类似指针的语义的 operator->() 非常有用. 例如:

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.

作者注: Excepted<> 类的声明加了 [[nodiscard]] 属性, 帮助做到了上述一点

相反, 以下代码将始终导致中止, 即使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 函数将按顺序访问每个处理程序, 并将其参数类型与错误的动态类型进行比较, 运行与之匹配的第一个处理程序. 这与决定运行哪个 catch 子句来处理 C++ 异常的决策过程相同.

由于传递给 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 函数来移除错误类型, 简化控制流程.

3.4.2.1 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, 甚至(尽管非常痛苦)考虑引入一个新的 std::error_code 作为权宜之计.

createStringError 函数可以接受 printf 样式的格式说明符, 以提供格式化的消息:

createStringError(errc::executable_format_error,
                  "Bad executable: %s", FileName);
3.4.2.2 std::error_code 和 ErrorOr 的互操作性

许多现有的LLVM API使用 std::error_code 及其伙伴 ErrorOr<T>(与 Expected<T> 扮演相同角色, 但包装的是 std::error_code 而不是 Error). 错误类型的传染性意味着将其中一个函数更改为返回 ErrorExpected<T> 通常会导致对调用者、调用者的调用者等的一系列更改. (首次尝试返回 ErrorMachOObjectFile 构造函数在差异达到 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>

3.4.2.3 从错误处理程序返回错误

错误恢复尝试本身可能会失败. 因此, handleErrors 实际上识别了三种不同形式的处理程序签名:

// 必须处理错误, 不会产生新的错误:
void(UserDefinedError &E);

// 必须处理错误, 可以产生新的错误:
Error(UserDefinedError &E);

// 可以检查原始错误, 然后重新封装并返回(或可以产生新的错误):
Error(std::unique_ptr<UserDefinedError> E);

从处理程序返回的任何错误都将从 handleErrors 函数返回, 以便可以对其进行处理, 或者向上传播到调用栈.

3.4.2.4 使用 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方法来设置一个可选的字符串“banner”作为前缀. 还可以使用 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, 它可以极大地提高可读性.

3.4.2.5 使用 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 函数, 但在 工具代码和单元测试代码 中, 它们可能更有用, 因为输入和/或模拟的类或函数可能被知道是安全的.

3.4.2.6 可失败的构造函数

某些类在构造过程中需要进行资源获取或其他复杂的初始化操作, 这些操作可能会失败. 不幸的是, 构造函数无法返回错误, 而在构造对象后让客户端进行测试以确保其有效性容易出错, 因为很容易忘记进行测试. 为了解决这个问题, 可以使用命名构造函数模式并返回一个 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 值的 checked 标志设置为 true , 以便可以对错误进行赋值, 然后在退出时重置为 false, 以强制客户端(即命名构造函数)检查错误.

通过使用这种模式, 试图构造 Foo 对象的客户端将收到一个良好构造的 Foo 对象或一个 Error 对象, 而不会收到处于无效状态的对象.

3.4.2.7 基于类型传播和消耗错误

在某些情况下, 某些类型的错误被认为是无害的. 例如, 在遍历存档时, 有些客户端可能愿意跳过格式错误的目标文件, 而不是立即终止遍历. 可以使用复杂的处理方法来跳过格式错误的对象, 但是 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();
}
3.4.2.8 使用 joinErrors 构造 Errors

在上面的存档遍历示例中, 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.

3.4.2.9 构建可失败迭代器和迭代器范围

上面的存档遍历示例通过索引检索存档成员, 但这需要大量的样板代码用于迭代和错误检查. 我们可以通过使用“可失败迭代器”模式来简化这个过程, 该模式支持以下自然的迭代方式, 适用于像 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;

为了实现这种方式, 对于可失败容器的迭代器, 我们需要使用可失败的 inc() 和 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 循环的主体中标记错误为已检查), 从而实现在循环中的早期退出, 避免冗余的错误检查.

使用 make_fallible_itr 和 make_fallible_end 函数对可失败迭代器接口的实例(例如上面的 FallibleChildIterator)进行包装. 例如:

class Archive {
public:
  using child_iterator = fallible_iterator<FallibleChildIterator>;

  child_iterator child_begin(Error &Err) {
    return make_fallible_itr(FallibleChildIterator(*this, 0), Err);
  }

  child_iterator child_end() {
    return make_fallible_end(FallibleChildIterator(*this, size()));
  }

  iterator_range<child_iterator> children(Error &Err) {
    return make_range(child_begin(Err), child_end());
  }
};

使用 fallible_iterator 工具不仅可以自然地构造可失败迭代器(使用失败的 inc 和 dec 操作), 还可以相对自然地使用 C++ 迭代器/循环习惯用法.

有关 Error 及其相关实用程序的更多信息可以在 Error.h 头文件中找到.

3.5 传递函数和其他可调用对象

有时您可能希望一个函数接受一个回调对象. 为了支持 lambda 表达式和其他函数对象, 不应该使用传统的 C 方法, 即采用一个函数指针和一个不透明的 cookie 参数:

void takeCallback(bool (*Callback)(Function *, void *), void *Cookie);

相反, 可以使用以下方法之一:

3.5.1 函数模板

如果您不介意将函数的定义放入头文件中, 可以将其定义为一个模板函数, 该模板函数的模板参数是可调用对象的类型.

template<typename Callable>
void takeCallback(Callable Callback) {
  Callback(1, 2, 3);
}

3.5.2 The function_ref class template

function_ref (doxygen) 类模板表示对可调用对象的引用, 模板化为可调用对象的类型. 如果您在函数返回后不需要保留回调函数, 那么这是将回调传递给函数的好选择. 在这种情况下, function_ref 类似于 std::function, 就像 StringRef 类似于 std::string 一样.

function_ref<Ret(Param1, Param2, ...)> 可以从任何可调用对象隐式构造, 该可调用对象可以使用类型为 Param1、Param2、… 的参数进行调用, 并返回一个可以转换为类型 Ret 的值. 例如:

void visitBasicBlocks(Function *F, function_ref<bool (BasicBlock*)> Callback) {
  for (BasicBlock &BB : *F)
    if (Callback(&BB))
      return;
}

可以这样调用:

visitBasicBlocks(F, [&](BasicBlock *BB) {
  if (process(BB))
    return isEmpty(BB);
  return false;
});

请注意, function_ref 对象包含对外部内存的指针, 因此通常不安全存储该类的实例(除非您知道外部存储不会被释放). 如果您需要这种能力, 请考虑使用 std::function. function_ref 的大小足够小, 应始终按值传递.

3.6 The LLVM_DEBUG() macro and -debug option

通常在处理自己的 Pass 时, 您会在其中添加一些调试打印和其他代码. 在使其正常工作后, 您希望将这些代码删除, 但将来可能需要再次使用(以解决您遇到的新错误).

自然而然地, 由于这个原因, 您不希望删除调试打印, 但也不希望它们一直存在. 一个标准的折中方案是将其注释掉, 以便在将来需要时启用它们.

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

LLVM_DEBUG(dbgs() << "I am here!\n");

Then you can run your pass like this:

$ 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 中启用或禁用它. 只需在程序运行时使用 “set DebugFlag=0” 或 “set DebugFlag=1” 即可. 如果程序尚未启动, 您始终可以使用 -debug 参数来运行它.

3.6.1 Fine grained debug info with DEBUG_TYPE and the -debug-only option

有时候您可能会发现启用 -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

Then you can run your pass like this:

$ 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 选项的参数.

出于性能原因, 在优化构建 (–enable-optimized) 的 LLVM 中不可用 -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");

3.7 The Statistic class & -stats option

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 基准测试套件中运行 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 与框架完美结合, 使其更易于维护和使用.

3.8 在调试代码时添加调试计数器可以帮助你进行调试

有时, 在编写新的 Pass 或追踪错误时, 能够控制代码中某些部分的执行与否是很有用的. 例如, 有时最小化工具只能给出大型测试用例. 你希望通过二分法将错误缩小到特定的转换操作的执行与否, 以便自动化调试. 这就是调试计数器的作用. 它们提供了一个框架, 使得代码的某些部分只执行特定次数.

在 LLVM 中, 你可以使用 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 选项来控制代码何时触发. 提供了两个计数器, 即 skip 和 count. skip 表示跳过执行代码路径的次数, count 表示在跳过后执行代码路径的次数.

$ opt --debug-counter=passname-delete-instruction-skip=1,passname-delete-instruction-count=2 -passname

这将在第一次执行时跳过上述代码, 然后执行两次, 然后跳过其余的执行.

因此, 如果在以下代码上执行:

%1 = add i32 %a, %b
%2 = add i32 %a, %b
%3 = add i32 %a, %b
%4 = add i32 %a, %b

它将删除 number % 2 和 number % 3.

utils/bisect-skip-count 提供了一个实用工具, 用于二分搜索跳过 (skip) 和计数 (count) 参数. 它可用于自动缩小调试计数器变量的跳过和计数值.

3.9 Viewing graphs while debugging code

LLVM 中的几个重要数据结构都是图形结构, 例如由 LLVM 的基本块构成的控制流图(CFG), 由LLVM的机器基本块构成的CFG, 以及指令选择图(Instruction Selection DAG). 在调试编译器的各个部分时, 即时可视化这些图形非常方便.

LLVM 在调试构建中提供了几个回调函数来实现这一点. 例如, 如果调用 Function::viewCFG() 方法, 当前的 LLVM 工具将弹出一个窗口, 其中包含函数的 CFG, 其中每个基本块是图中的一个节点, 每个节点包含块中的指令. 类似地, 还存在 Function::viewCFGOnly()(不包括指令)、 MachineFunction::viewCFG() 和 MachineFunction::viewCFGOnly(), 以及SelectionDAG::viewGraph()方法. 在 GDB 中, 例如, 通常可以使用类似于 call DAG.viewGraph() 的方式弹出一个窗口. 或者, 您可以在代码中的调试位置添加对这些函数的调用.

要使其工作, 需要进行一些设置. 在具有 X11 的 Unix 系统上, 安装 graphviz 工具包, 并确保 dotgv 在您的路径中. 如果您在 macOS 上运行, 请下载并安装 macOS 的 Graphviz 程序, 并将 /Applications/Graphviz.app/Contents/MacOS/ (或您安装的位置)添加到您的路径中. 这些程序在配置、构建或运行 LLVM 时不必存在, 而可以在活动调试会话期间根据需要安装.

SelectionDAG 已扩展, 使在大型复杂图形中定位感兴趣的节点更加容易. 从 gdb 中, 如果调用 DAG.setGraphColor(node, "color"), 那么接下来的DAG.viewGraph()调用将以指定的颜色突出显示该节点(可以在colors中找到颜色选择). 更复杂的节点属性可以使用调用 DAG.setGraphAttrs(node, "attributes") 提供(可以在Graph attributes中找到选择). 如果您想重新启动并清除所有当前的图形属性, 那么可以调用 DAG.clearGraphAttrs().

请注意, 图形可视化功能在发布版本中被编译取消以减小文件大小. 这意味着您需要使用 Debug+AssertsRelease+Asserts 构建来使用这些功能.

4 Picking the Right Data Structure for a Task

LLVM在 llvm/ADT/ 目录下有各种各样的数据结构, 我们通常使用STL数据结构. 本节将介绍在选择数据结构时需要考虑的权衡因素.

首先, 你需要决定使用顺序容器、 set-like 的容器还是 map-like 的容器. 在选择容器时最重要的是要考虑你计划如何访问容器的算法特性. 基于这一点, 你应该选择以下之一:

  • 如果你需要根据另一个值高效地查找一个值, 可以使用 map-like 的容器. map-like 的容器也支持高效地查询是否包含某个键. map-like 的容器通常不支持高效的反向映射(值到键的映射). 如果需要反向映射, 可以使用两个映射. 某些 map-like 的容器还支持按照键的排序顺序高效地迭代访问键. map-like 的容器是最昂贵的一类容器, 只有在需要这些功能之一时才使用它们.
  • 如果你需要将一堆元素放入容器并自动消除重复项, 可以使用 set-like 的容器. 某些 set-like 的容器支持按照排序顺序高效地迭代访问元素. set-like 的容器比顺序容器更昂贵.
  • 顺序容器提供了最高效的添加元素的方式, 并且会记录它们被添加到集合中的顺序. 它们允许重复项并支持高效的迭代访问, 但不支持基于键的高效查找.
  • 字符串容器是用于字符或字节数组的专用顺序容器或引用结构.
  • 位容器提供了一种在数字ID集上存储和执行集合操作的高效方式, 并自动消除重复项. 位容器对于存储每个要存储的标识符最多只需要1位.

确定了合适的容器类别后, 你可以通过智能选择类别中的成员来调整内存使用、常数因子和缓存访问行为. 需要注意的是, 常数因子和缓存行为可能非常重要. 例如, 如果你的向量通常只包含少量元素(但可能包含许多元素), 使用SmallVector要比vector好得多. 这样做避免了(相对)昂贵的 malloc/free 调用, 这比将元素添加到容器中的成本要高得多.

4.1 Sequential Containers (std::vector, std::list, etc)

根据你的需求, 有各种顺序容器可供选择. 选择本节中满足你需求的第一个容器.

4.1.1 llvm/ADT/ArrayRef.h

llvm::ArrayRef 类是在接口中使用的首选类, 它接受一个顺序存储的元素列表并只对其进行读取操作. 通过接受 ArrayRef, API 可以接受固定大小的数组、std::vector、llvm::SmallVector 和其他在内存中连续的数据结构.

4.1.2 Fixed Size Arrays

固定大小数组非常简单且非常快速. 如果您确切地知道有多少元素, 或者对元素数量有一个(较低的)上限, 那么固定大小数组是很好的选择.

4.1.3 Heap Allocated Arrays

堆分配的数组(new[] + delete[])也很简单. 如果元素数量是可变的, 如果在数组分配之前知道需要多少元素, 并且数组通常很大(如果不是, 考虑使用 SmallVector), 那么堆分配的数组是很好的选择. 堆分配数组的成本是 new/delete(即 malloc/free)的成本. 还要注意, 如果您正在分配一个具有构造函数的类型的数组, 构造函数和析构函数将针对数组中的每个元素运行(可调整大小的向量仅构造实际使用的那些元素).

4.1.4 llvm/ADT/TinyPtrVector.h

TinyPtrVector<Type> 是一种高度专门化的集合类, 它在向量没有元素或只有一个元素的情况下进行了优化, 以避免分配内存. 它有两个主要限制:1)它只能容纳指针类型的值, 2)它不能容纳空指针.

由于这个容器是高度专门化的, 很少被使用.

4.1.5 llvm/ADT/SmallVector.h

SmallVector<Type, N> 是一个简单的类, 看起来和使用 vector 类似:它支持高效的迭代, 按照内存顺序布局元素(因此可以在元素之间进行指针算术运算), 支持高效的 push_back/pop_back 操作, 支持对元素进行高效的随机访问等等.

SmallVector 的主要优势在于它在对象本身中分配了一些元素(N)的空间. 因此, 如果 SmallVector 动态大小小于 N, 就不会执行 malloc 操作. 这在 malloc/free 调用的开销比操作元素的代码更大的情况下是一个巨大的优势.

这适用于“通常较小”的向量(例如, 块的前驱/后继数通常小于 8). 另一方面, 这使得 SmallVector 本身的大小较大, 因此不要分配大量的 SmallVector(这样会浪费很多空间). 因此, 在堆栈上使用 SmallVector 最为有用.

如果没有对于内联元素数量 N 的充分理由的选择, 建议使用 SmallVector<T>(即省略 N). 这将选择一个默认的内联元素数量, 适合在堆栈上进行分配(例如, 尝试将 sizeof(SmallVector<T>) 保持在 64 字节左右).

SmallVector 还提供了一个很好的可移植和高效的替代 alloca 的方法.

SmallVector 在一些其他方面也比 std::vector 有一些小优势, 这导致 SmallVector<Type, 0> 被优先选择而不是 std::vector<Type>.

std::vector 是异常安全的, 并且一些实现在需要移动元素时会进行复制, 而 SmallVector 则会移动元素.
SmallVector 理解 std::is_trivially_copyable<Type>(平凡可复制), 并积极使用 realloc.
许多 LLVM API 将 SmallVectorImpl 作为输出参数(请参阅下面的说明).
在 64 位平台上, SmallVector 的 N 等于 0 比 std::vector 更小, 因为它使用 unsigned(而不是 void*)来表示大小和容量.


注意:
优先使用 ArrayRef<T>SmallVectorImpl<T> 作为参数类型.

很少有情况适合使用 SmallVector<T, N> 作为参数类型. 如果一个 API 只从向量中读取数据, 应该使用 ArrayRef. 即使一个 API 更新向量, “small size” 也不太相关;这种情况下应该使用 SmallVectorImpl<T> 类, 它是 “vector header”(和方法)而不是在其之后分配元素的部分. 需要注意的是, SmallVector<T, N> 继承自 SmallVectorImpl, 所以转换是隐式的且没有额外开销. 例如:

// 不推荐使用:客户端不能传递原始数组. 
hardcodedContiguousStorage(const SmallVectorImpl<Foo> &In);
// 鼓励使用:客户端可以传递任何连续的 Foo 存储
allowsAnyContiguousStorage(ArrayRef<Foo> In);

void someFunc1() {
  Foo Vec[] = { /* ... */ };
  hardcodedContiguousStorage(Vec); // Error.
  allowsAnyContiguousStorage(Vec); // Works.
}

// 不推荐使用:客户端不能传递例如 SmallVector<Foo, 8>
hardcodedSmallSize(SmallVector<Foo, 2> &Out);
// 鼓励使用:客户端可以传递任何 SmallVector<Foo, N>
allowsAnySmallSize(SmallVectorImpl<Foo> &Out);

void someFunc2() {
  SmallVector<Foo, 8> Vec;
  hardcodedSmallSize(Vec); // Error.
  allowsAnySmallSize(Vec); // Works.
}

尽管名字中有 “Impl”, 但 SmallVectorImpl 已经被广泛使用, 并不再是 “private to the implementation”. 可能更合适的名称是 SmallVectorHeader.

4.1.6 vector

std::vector<T> 备受喜爱和尊重. 然而, 由于上述优势, SmallVector<T, 0> 通常是一个更好的选择. 当您需要存储超过 UINT32_MAX 个元素或与期望使用向量的代码进行接口时, std::vector 仍然很有用.

关于 std::vector 值得一提的一点是:避免编写以下类似的代码:

for ( ... ) {
   std::vector<foo> V;
   // make use of V.
}

Instead, write this as:

std::vector<foo> V;
for ( ... ) {
   // make use of V.
   V.clear();
}

这样做将在每次循环迭代中节省(至少)一次堆分配和释放操作.

4.1.7 deque

从某种意义上说, std::dequestd::vector 的一个通用版本. 与 std::vector 一样, 它提供了常数时间的随机访问和其他类似的特性, 但它还提供了对列表前端的高效访问. 然而, 它不保证元素在内存中的连续性.

为了获得这种额外的灵活性, std::deque 的常数因子成本明显较高. 如果可能的话, 请使用 std::vector 或其他更经济的容器.

4.1.8 list

std::list 是一个极其低效的类, 很少有用. 它对每个插入的元素执行堆分配, 因此具有非常高的常数因子, 特别是对于小型数据类型而言. std::list 只支持双向迭代, 不支持随机访问迭代.

作为代价, std::list 支持对列表两端的高效访问(类似于 std::deque, 但与 std::vectorSmallVector 不同). 此外, std::list 的迭代器失效特性比向量类更强:在列表中插入或删除元素不会使迭代器或指向其他元素的指针失效.

4.1.9 llvm/ADT/ilist.h

ilist<T> 实现了一种“内嵌”的双向链表. 它是内嵌的, 因为它要求元素存储并提供对列表的前后指针的访问.

ilist 具有与 std::list 相同的缺点, 并且还需要为元素类型实现 ilist_traits, 但它提供了一些新颖的特性. 特别是, 它可以高效地存储多态对象, 当元素从列表中插入或删除时, traits 类会被通知, 并且 ilist 保证支持常数时间的剪接操作.

ilist 和 iplist 互相使用别名, 而 iplist 目前仅出于历史目的而存在.

这些特性正是我们在指令和基本块等方面所需要的, 这也是为什么它们使用 ilist 进行实现的.

有关感兴趣的相关类, 请参阅以下子节:

4.1.10 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'.
}

4.1.11 ilist_traits

ilist_traits<T>ilist<T> 的自定义机制. ilist<T> 公开继承自这个 traits 类.

4.1.12 llvm/ADT/ilist_node.h

ilist_node<T> 实现了 ilist<T>(和类似的容器)所需的前向和后向链接, 默认方式.

ilist_node<T> 应该嵌入在节点类型 T 中, 通常 T 公开继承自 ilist_node<T>

4.1.13 Sentinels

ilist 在考虑到 C++ 生态系统中的规范时还有另一个特殊性需要考虑. 它需要支持标准的容器操作, 例如 begin 和 end 迭代器等. 此外, 在非空 ilist 中, operator-- 必须正确地工作于末尾迭代器.

解决这个问题的唯一合理方法是在 intrusive list 旁边分配一个称为 sentinel 的元素, 作为末尾迭代器, 并提供到最后一个元素的后向链接. 然而, 根据 C++ 的约定, 不允许在 sentinel 之后进行 operator++ 操作, 并且不得对其进行解引用.

这些限制允许 ilist 在如何分配和存储 sentinel 方面有一定的实现自由度. 相应的策略由 ilist_traits<T> 指定. 默认情况下, 当需要 sentinel 时, 会为 T 进行堆分配.

尽管默认策略在大多数情况下足够, 但在 T 不提供默认构造函数的情况下可能会出现问题. 此外, 在存在多个 ilist 实例的情况下, 与其关联的 sentinel 的内存开销是浪费的. 为了缓解大量且庞大的 T-sentinel 问题, 有时会使用一种技巧, 即使用 ghostly sentinel.

通过特殊设计的 ilist_traits<T> 可以获得 ghostly sentinel, 它将 sentinel 与 ilist 实例内存上叠加. 使用指针运算可以获取 sentinel, 该 sentinel 相对于 ilist 的 this 指针. ilist 通过额外的指针进行扩展, 该指针充当 sentinel 的后向链接. 这是 ghostly sentinel 中唯一可以合法访问的字段.

4.1.14 其他顺序容器选项

还有其他的STL容器可用, 例如 std::string.

此外, 还有各种STL适配器类, 例如 std::queue 、 std::priority_queue、std::stack 等. 它们提供对底层容器的简化访问, 但不会影响容器本身的成本.

4.2 String-like containers

有多种方法可以在C和C++中传递和使用字符串, 而LLVM添加了一些新的选择. 根据相对成本的顺序, 选择能够满足需求的列表中的第一个选项.

请注意, 通常最好不要将字符串作为 const char 传递. 这种方式存在一些问题, 包括不能表示嵌入的空字符(“0”)和没有有效获取长度的能力. 用于替代’const char’的通用选择是 StringRef.

有关为API选择字符串容器的更多信息, 请参阅 传递字符串.

4.2.1 llvm/ADT/StringRef.h

StringRef 类是一个简单的值类, 包含一个指向字符的指针和一个长度, 并且与ArrayRef 类非常相关(但专门用于字符数组). 由于 StringRef 携带了长度信息, 它可以安全地处理包含空字符的字符串, 获取长度不需要调用 strlen 函数, 而且它还提供了非常方便的API来对表示的字符范围进行切片和处理.

StringRef 非常适合传递简单的字符串, 这些字符串被知道是有效的, 无论是因为它们是 C 字符串字面量、std::string、C数组还是SmallVector. 这些情况下都存在一个高效的隐式转换到 StringRef 的方式, 不需要执行动态的 strlen 调用.

然而, StringRef 也有一些主要限制, 这就需要更强大的字符串容器:

您无法直接将 StringRef 转换为’const char*', 因为没有方法添加尾随的空字符(不像更强大的类上的 .c_str() 方法).
StringRef 不拥有或保持底层字符串字节的生存期. 因此, 它很容易导致悬空指针, 并且在大多数情况下不适合嵌入在数据结构中(而是使用 std::string 或类似的类型).
出于相同的原因, 如果方法“计算”结果字符串, StringRef 不能用作方法的返回值. 而应该使用 std::string.
StringRef 不允许您更改指向的字符串字节, 也不允许您在该范围内插入或删除字节. 对于此类编辑操作, 它与 Twine 类进行交互.
由于它的优点和限制, 函数通常会接受一个 StringRef 参数, 并且对象的方法会返回一个指向它拥有的某个字符串的 StringRef.

4.2.2 llvm/ADT/Twine.h

Twine 类用作中间数据类型, 用于那些希望使用一系列连接操作内联构造字符串的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 非常配合. 只要注意它们的限制即可.

4.2.3 llvm/ADT/SmallString.h

SmallString 是 SmallVector 的子类, 它添加了一些方便的API, 比如 += , 接受 StringRef. 当预分配的空间足以容纳数据时, SmallString 避免了内存分配, 并在需要时回退到一般的堆分配. 由于它拥有自己的数据, 所以非常安全可靠, 并支持对字符串的完全修改.

与 SmallVector 一样, SmallString 的一个主要缺点是它们的 sizeof 大小. 虽然它们被优化用于小字符串, 但它们本身并不特别小. 这意味着它们非常适合用作栈上的临时缓冲区, 但通常不应该放入堆中:很少见到 SmallString 作为频繁分配的堆数据结构的成员或者作为返回值.

4.2.4 std::string

标准 C++ 的 std::string 类是一个非常通用的类, 它(和SmallString一样)拥有其底层数据. sizeof(std::string) 非常合理, 因此可以将其嵌入到堆数据结构中并作为返回值. 另一方面, std::string 在内联编辑(例如将一堆内容拼接在一起)方面效率非常低下, 并且由于它是由标准库提供的, 其性能特性很大程度上取决于宿主标准库(例如 libc++ 和 MSVC 提供了高度优化的字符串类, GCC则包含了一个非常慢的实现).

std::string 的主要缺点是几乎每个使其变大的操作都会分配内存, 这是很慢的. 因此, 最好使用 SmallVector 或 Twine 作为临时缓冲区, 然后使用 std::string 来保存结果.


4.3 Set-Like Containers (std::set, SmallSet, SetVector, etc)

当您需要将多个值归一化为单个表示时, 集合类容器非常有用. 在这方面, 有几种不同的选择, 提供了各种权衡取舍的可能性.

4.3.1 A sorted ‘vector’

如果您打算插入大量元素, 然后进行大量查询, 一个很好的方法是使用std::vector(或其他顺序容器), 结合std::sort+std::unique来去除重复项. 如果您的使用模式具有这两个明确的阶段(插入然后查询), 这种方法效果非常好, 并且可以与良好选择的顺序容器相结合使用.

这种组合提供了几个优点:结果数据在内存中是连续的(有利于缓存局部性), 内存分配较少, 易于寻址(最终向量中的迭代器只是索引或指针), 并且可以使用标准的二分搜索(例如std::lower_bound;如果您想要比较相等的整个元素范围, 请使用std::equal_range)进行高效的查询.

4.3.2 llvm/ADT/SmallSet.h

如果您有一个通常很小且元素相对较小的类似集合的数据结构, 那么 SmallSet<Type, N> 是一个很好的选择. 该集合在原地为N个元素提供空间(因此, 如果集合在动态上小于N, 就不需要进行内存分配), 并使用简单的线性搜索访问它们. 当集合的大小超过N个元素时, 它会分配一个更昂贵的表示形式, 以确保高效访问(对于大多数类型, 它会回退到std::set, 但对于指针, 它使用的是更好的SmallPtrSet.

这个类的魔力在于它能够非常高效地处理小型集合, 同时在不损失效率的情况下优雅地处理极大型集合.

4.3.3 llvm/ADT/SmallPtrSet.h

SmallPtrSet 具 SmallSet 的所有优点(而且SmallSet的指针集合会透明地使用SmallPtrSet 进行实现). 如果执行的插入操作超过N次, 将分配一个单一的二次探测哈希表, 并根据需要进行扩展, 提供极其高效的访问(具有低常数因子的常数时间插入/删除/查询)并且非常节约内存分配.

请注意, 与 std::set 不同, 当进行插入操作时, SmallPtrSet 的迭代器将失效. 此外, 迭代器访问的值不会按排序顺序访问.

4.3.4 llvm/ADT/StringSet.h

StringSet 是 StringMap<char> 的一个轻量级包装, 它可以高效地存储和检索唯一的字符串.

在功能上类似于 SmallSet<StringRef>, StringSet 还支持迭代(迭代器解引用为 StringMapEntry<char> , 因此需要调用 i->getKey() 来访问StringSet的项). 另一方面, StringSet不支持范围插入和复制构造, 而 SmallSet 和 SmallPtrSet 支持这些操作.

4.3.5 llvm/ADT/DenseSet.h

DenseSet 是一个简单的二次探测哈希表. 它非常适用于支持小值:它使用单个分配来存储当前插入到集合中的所有键值对. DenseSet 是唯一化非简单指针的小值的理想选择(对于指针, 请使用SmallPtrSet). 请注意, DenseSet对于值类型的要求与 DenseMap相同.

4.3.6 llvm/ADT/SparseSet.h

SparseSet 是通过中等大小的无符号键标识的少量对象. 它使用了大量内存, 但提供的操作几乎和向量一样快速. 典型的键可以是物理寄存器、虚拟寄存器或编号的基本块.

SparseSet 对于需要非常快速的清除、查找、插入和删除操作以及对小集合进行快速迭代的算法非常有用. 它不适用于构建复合数据结构.

4.3.7 llvm/ADT/SparseMultiSet.h

SparseMultiSet 在保留 SparseSet 的优点的基础上, 添加了多重集合(multiset)的行为. 与 SparseSet 类似, 它通常使用了大量的内存, 但提供的操作几乎和向量一样快速. 典型的键可以是物理寄存器、虚拟寄存器或编号的基本块.

SparseMultiSet 对于需要非常快速的清除、查找、插入和删除整个集合以及对共享键的元素集进行迭代的算法非常有用. 与使用复合数据结构(例如向量的向量、向量的映射)相比, 它通常是更高效的选择. 它不适用于构建复合数据结构.

4.3.8 llvm/ADT/FoldingSet.h

FoldingSet 是一个聚合类, 非常适用于唯一化昂贵或多态对象. 它是一个使用了 链式哈希表 和内部链接(唯一化的对象需要继承自 FoldingSetNode )的组合, 其中在其 ID 处理过程中使用了SmallVector.

考虑这样一种情况:你想为一个复杂对象(例如代码生成器中的一个节点)实现一个“getOrCreateFoo”方法. 客户端具有要生成的内容的描述(它知道操作码和所有操作数), 但我们不想先’new’一个节点, 然后尝试将其插入到集合中, 只有在发现它已经存在时, 我们才需要删除它并返回已经存在的节点.

为了支持这种客户端方式, FoldingSet使用FoldingSetNodeID(它包装了SmallVector)进行查询, 该ID用于描述我们要查询的元素. 查询要么返回与ID匹配的元素, 要么返回一个不透明的ID, 指示应该进行插入的位置. 构造ID通常不需要堆上的内存操作.

由于FoldingSet使用内部链接, 它可以支持集合中的多态对象(例如, 你可以将SDNode实例与LoadSDNode混合使用). 由于元素是单独分配的, 指向元素的指针是稳定的:插入或删除元素不会使指向其他元素的指针失效.

4.3.9 set

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

std::set 的优点是它的迭代器是稳定的(从集合中删除或插入元素不会影响其他元素的迭代器或指针), 并且对集合的迭代保证按照排序顺序进行. 如果集合中的元素很大, 那么指针和 malloc 开销的相对额外开销并不是一个大问题, 但如果集合的元素很小, std::set几乎永远不是一个好的选择.

4.3.10 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, 将会节省大量的堆内存开销.

4.3.11 llvm/ADT/UniqueVector.h

UniqueVector 类类似于 SetVector, 但它为插入集合的每个元素保留了唯一的ID. 它内部包含一个映射(map)和一个向量(vector), 并为插入集合的每个值分配一个唯一的ID.

UniqueVector非常昂贵:它的成本是维护映射和向量的成本之和, 具有较高的复杂度、高的常数因子, 并产生大量的堆内存开销. 应尽量避免使用它.

4.3.12 llvm/ADT/ImmutableSet.h

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

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

4.3.13 Other Set-Like Container Options

STL 提供了其他几种选项, 比如 std::multiset 和 std::unordered_set. 我们从不使用类似 unordered_set 的容器, 因为它们通常非常昂贵(每次插入都需要进行内存分配).

std::multiset 在您不关心消除重复项时很有用, 但它也具有 std::set 的所有缺点. A sorted vector (其中不删除重复条目)或其他方法几乎总是更好的选择.

4.4 Map-Like Containers (std::map, DenseMap, etc)

当您想要将数据与键关联起来时, 类似映射的容器非常有用. 通常情况下, 有很多不同的方法可以实现这个目标. 😃

4.4.1 A sorted ‘vector’

如果您的使用模式遵循严格的 插入-查询 方法, 您可以轻松地使用与有序向量相同的方法来处理类似集合的容器. 唯一的区别是您的查询函数(使用std::lower_bound 进行高效的对数时间查找)应仅比较键而不是键和值. 这样可以获得与有序向量相同的优势.

4.4.2 llvm/ADT/StringMap.h

字符串在映射中通常用作键, 但要高效地支持它们并不容易:它们的长度可变, 在长度较长时哈希和比较效率低下, 复制开销大等等. StringMap是一个专门处理这些问题的特殊容器. 它支持将任意字节范围映射到任意其他对象.

StringMap 的实现使用了一种 二次探测的哈希表, 其中的存储了指向堆分配条目的指针(以及其他一些内容). 由于字符串的长度可变, 映射中的条目必须进行堆分配. 字符串数据(键)和元素对象(值)与字符串数据紧随元素对象之后一起存储在同一分配中. 该容器保证了对于值来说, “(char*)(&Value+1)” 指向键字符串.

StringMap 之所以非常快速, 原因如下:二次探测对于查找非常高效, 桶中的字符串哈希值在查找元素时不会重新计算, StringMap 在查找值时很少需要访问与之无关的内存(即使哈希冲突发生时也是如此), 哈希表的增长不会重新计算已经存在于表中的字符串的哈希值, 而且映射中的每对元素都存储在单个分配中(字符串数据存储在与一对的值相同的分配中).

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

然而, StringMap 的 迭代顺序不能保证是确定性的, 因此任何需要确定性迭代顺序的用途应改用 std::map.

4.4.3 llvm/ADT/IndexedMap.h

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

这在像 LLVM 代码生成器中的虚拟寄存器这样的情况下非常有用:它们具有通过编译时常量(第一个虚拟寄存器ID)进行偏移的密集映射.

4.4.4 llvm/ADT/DenseMap.h

DenseMap 是一个简单的 二次探测哈希表, 非常适用于支持小键和小值:它使用单个分配来保存当前插入到映射中的所有键值对. DenseMap 是将指针映射到指针或将其他小类型相互映射的理想选择.

然而, 有几个方面需要注意. DenseMap 中的迭代器在插入操作发生时会失效, 与map不同. 此外, 由于 DenseMap 为大量的 键/值 对分配空间(默认情况下从64开始), 如果键或值较大, 将会浪费很多空间. 最后, 如果您的键类型没有得到支持, 您需要为所需的键实现 DenseMapInfo 的偏特化. 这是为了向 DenseMap 介绍两个内部所需的特殊标记值(永远不能插入到映射中).

DenseMap 的 find_as() 方法支持使用替代键类型进行查找操作. 这在正常键类型的构造成本较高, 但与之进行比较的成本很低的情况下非常有用. DenseMapInfo 负责为使用的每个替代键类型定义适当的比较和哈希方法.

4.4.5 llvm/IR/ValueMap.h

ValueMap 是一个包装器, 围绕着一个 DenseMap , 将 Value*(或其子类)映射到另一种类型. 当删除或替换 Value 时, ValueMap 会自动更新, 使新版本的键映射到相同的值, 就像键是 WeakVH 一样. 您可以通过向ValueMap模板传递一个 Config 参数来精确配置此过程以及在这两个事件发生时的其他操作.

4.4.6 llvm/ADT/IntervalMap.h

IntervalMap 是用于小型键和值的紧凑映射. 它将键区间映射而不是单个键, 并且会自动合并相邻的区间. 当映射只包含少量区间时, 它们存储在映射对象本身中, 以避免分配内存.

IntervalMap 的迭代器相当庞大, 因此不应将其作为STL迭代器传递. 重型迭代器允许使用较小的数据结构.

4.4.7 llvm/ADT/IntervalTree.h

llvm::IntervalTree 是一种轻量级的树形数据结构, 用于保存区间. 它允许查找与给定点重叠的所有区间. 目前, 它不支持任何删除或平衡操作.

IntervalTree 的设计初衷是在设置后进行查询, 而无需进一步添加操作.

4.4.8 map

std::map 具有与 std::set 类似的特性:它对每个插入到映射中的键值对使用单个分配, 提供了具有极大常数因子的对数级别的查找, 对于每个键值对在映射中需要3个指针的空间开销等.

当键或值非常大时, 如果您需要按排序顺序迭代集合, 或者如果您需要对映射进行稳定的迭代器(即在插入或删除其他元素时不会使其失效), std::map 是非常有用的.

4.4.9 llvm/ADT/MapVector.h

MapVector<KeyT, ValueT> 提供了 DenseMap 接口的子集. 主要区别在于迭代顺序保证是插入顺序, 使其成为非确定性指针映射的简单(但有些昂贵)解决方案.

它的实现方式是通过将键映射到键值对向量中的索引. 这提供了快速的查找和迭代, 但有两个主要缺点:键被存储两次, 并且删除元素需要线性时间. 如果需要删除元素, 最好使用 remove_if() 批量删除.

4.4.10 llvm/ADT/IntEqClasses.h

IntEqClasses 提供了小整数的等价类的紧凑表示. 初始情况下, 范围0到n-1中的每个整数都有自己的等价类. 可以通过将两个类的代表传递给join(a, b)方法来合并类. 当 findLeader() 返回相同的代表时, 两个整数属于同一个类.

一旦所有等价类形成, 可以压缩映射, 使得0到n-1中的每个整数映射到0到m-1范围内的等价类编号, 其中m是总等价类的数量. 在可以再次编辑之前, 必须取消压缩映射.

4.4.11 llvm/ADT/ImmutableMap.h

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

4.4.12 Other Map-Like Container Options

STL提供了其他几个选项, 例如 std::multimap 和 std::unordered_map. 我们通常不使用类似 unordered_map 的容器, 因为它们通常非常昂贵(每个插入操作都需要分配内存).

如果你想将一个键映射到多个值, std::multimap 是有用的, 但它也具有 std::map 的所有缺点. 排序向量或其他方法通常更好.

4.5 Bit storage containers

有几种位存储容器可供选择, 选择何时使用每个容器相对简单.

另一个选项是 std::vector<bool>:我们不鼓励使用它, 有两个原因:1)许多常见编译器(例如常见的GCC版本)中的实现效率非常低下;2)C++ 标准委员会很可能会弃用此容器和/或以某种方式进行重大更改. 无论如何, 请不要使用它.

4.5.1 BitVector

BitVector 容器提供了一个用于操作的动态大小位集合. 它支持单个位的设置和测试, 以及集合操作. 集合操作的时间复杂度为 O(bitvector的大小), 但操作是以字为单位进行的, 而不是以位为单位. 与其他容器相比, 这使得 BitVector 在集合操作方面非常快速. 当您预计设置位数较高(即密集集合)时, 请使用 BitVector.

4.5.2 SmallBitVector

SmallBitVector 容器提供与 BitVector 相同的接口, 但它针对只需要少量位(大约小于25位)的情况进行了优化. 它还可以透明地支持更大的位数, 但效率略低于普通的 BitVector, 因此 SmallBitVector 只应在较大的位数很少出现的情况下使用.

目前, SmallBitVector 不支持集合操作(与、或、异或), 它的 operator[] 也不提供可赋值的左值.

4.5.3 SparseBitVector

SparseBitVector 容器与 BitVector 非常相似, 但有一个主要区别:它只存储已设置的位. 当集合稀疏时, 这使得 SparseBitVector 在空间利用上比 BitVector 更高效, 同时使得集合操作的时间复杂度为O(设置位的数量), 而不是O(size of universe). SparseBitVector 的缺点是随机位的设置和测试时间复杂度为O(N), 在大型 SparseBitVector 上, 这可能比 BitVector 更慢. 在我们的实现中, 按排序顺序(向前或向后)设置或测试位的最坏情况时间复杂度为O(1). 在当前位的前后128位(取决于大小)范围内测试和设置位也是O(1). 一般而言, 在 SparseBitVector 中测试/设置位的时间复杂度为O(与上一个设置位的距离).

4.5.4 CoalescingBitVector

CoalescingBitVector 容器在原理上与 SparseBitVector 类似, 但经过优化, 可以紧凑地表示大范围连续的置位位. 它通过将连续的置位位范围合并成间隔来实现这一点. 在 CoalescingBitVector 中搜索位的时间复杂度为 O(log(连续范围之间的间隔)).

当置位位范围之间的间隔较大时, CoalescingBitVector 是比 BitVector 更好的选择. 当需要快速、可预测的 find() 操作性能时, 它比 SparseBitVector 更合适. 然而, 它不适合表示具有大量非常短范围的集合. 例如, 集合 {2*x: x 在 [0, n) 范围内} 将是一个病态输入.

4.6 Useful Utility Functions

LLVM实现了许多通用的实用函数, 这些函数在代码库中被广泛使用. 你可以在STLExtras.h(doxygen)中找到最常见的函数. 其中一些函数封装了众所周知的C++标准库函数, 而另一些函数则是LLVM独有的.

4.6.1 Iterating over ranges

有时候您可能希望一次迭代多个范围, 或者知道索引的索引. LLVM提供了自定义的实用函数来简化这个过程, 而无需手动管理所有的迭代器和/或索引:

4.6.1.1 The zip* functions

zip*函数允许同时迭代两个或多个范围的元素. 例如:

SmallVector<size_t> Counts = ...;
char Letters[26] = ...;
for (auto [Letter, Count] : zip_equal(Letters, Counts))
errs() << Letter << ": " << Count << "\n";

请注意, 元素通过“引用包装器”代理类型(引用的元组)提供, 结合结构化绑定声明, 使得 Letter 和 Count 成为范围元素的引用. 对这些引用的任何修改都会影响 Letters 或 Counts 的元素.

zip*函数支持临时范围, 例如:

for (auto [Letter, Count] : zip(SmallVector<char>{'a', 'b', 'c'}, Counts))
errs() << Letter << ": " << Count << "\n";

zip系列函数之间的区别在于当提供的范围具有不同长度时它们的行为:

  • zip_equal - 要求所有输入范围具有相同的长度.
  • zip - 当达到最短范围的末尾时停止迭代.
  • zip_first - 要求第一个范围是最短的范围.
  • zip_longest - 迭代直到达到最长范围的末尾. 较短范围中不存在的元素将被替换为std::nullopt.

长度要求通过断言进行检查.

作为经验法则, 如果您希望所有范围具有相同的长度, 请优先使用 zip_equal, 并且仅在不满足此条件时考虑使用其他 zip 函数. 这是因为 zip_equal 清楚地传达了这种相同长度的假设, 并且具有最佳(发布模式下的)运行时性能.

4.6.1.2 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 要求它们具有相等的长度(通过断言进行检查).

5 Debugging

为一些核心LLVM库提供了一些 GDB漂亮打印器. 要使用它们, 请执行以下命令(或将其添加到您的 ~/.gdbinit 文件中):

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

另外, 启用打印漂亮选项可能会很方便, 以避免将数据结构打印为大块文本.

6 Helpful Hints for Common Operations

本节介绍如何执行一些非常简单的 LLVM 代码转换. 这旨在通过展示 LLVM 转换的实际示例, 展示常见的习惯用法.

因为这是一个“如何”部分, 您还应该阅读您将使用的主要类的相关信息. LLVM核心类层次结构参考 提供了您应该了解的主要类的详细信息和描述.

6.1 Basic Inspection and Traversal Routines

LLVM 编译器基础设施拥有许多不同的数据结构, 可以进行遍历. 按照 C++ 标准模板库的例子, 用于遍历这些不同的数据结构的技术基本上是相同的. 对于可枚举的值序列, XXXbegin()函数(或方法)返回指向序列开头的迭代器, XXXend()函数返回指向序列中最后一个有效元素之后的迭代器, 并且存在某种 XXXiterator 数据类型, 该类型在这两个操作之间是通用的.

由于迭代模式在程序表示的许多不同方面都是常见的, 因此可以在它们上使用标准模板库算法, 并且更容易记住如何进行迭代. 首先, 我们展示了一些需要遍历的常见数据结构的示例. 其他数据结构的遍历方式非常类似.

6.1.1 Iterating over the BasicBlock in a Function

经常会有一个 Function 实例, 您希望以某种方式对其进行转换;特别是, 您希望操作其中的 BasicBlock. 为了实现这一点, 您需要遍历组成 Function 的所有 BasicBlock. 以下是一个示例, 打印 BasicBlock 的名称和其中包含的指令数:

Function &Func = ...
for (BasicBlock &BB : Func)
  // 如果BasicBlock有名称, 则打印其名称, 然后打印其中包含的指令数
  errs() << "Basic block (name=" << BB.getName() << ") has "
    << BB.size() << " instructions.\n";

6.1.2 Iterating over the Instruction in a BasicBlock

就像处理函数中的基本块(BasicBlock)时一样, 迭代构成基本块的单个指令也很容易. 以下是一个代码片段, 用于打印出基本块中的每条指令:

BasicBlock& BB = ...
for (Instruction &I : BB)
   // The next statement works since operator<<(ostream&,...)
   // is overloaded for Instruction&
   errs() << I << "\n";

然而, 这并不是打印 BasicBlock 内容的最佳方式!由于 ostream 运算符已经针对您关心的几乎所有内容进行了重载, 您可以直接在 BasicBlock 本身上调用打印例程:errs() << BB << "\n";.

6.1.3 Iterating over the Instruction in a Function

如果您发现自己经常迭代 Function 的 BasicBlocks , 然后迭代 BasicBlock 的 Instructions, 应该使用 InstIterator. 您需要包含 llvm/IR/InstIterator.h(doxygen), 然后在代码中显式实例化 InstIterator . 以下是一个小例子, 展示了如何将一个函数中的所有指令转储到标准错误流中:

#include "llvm/IR/InstIterator.h"

// F是一个指向 Function 实例的指针
for (inst_iterator I = inst_begin(F), E = inst_end(F); I != E; ++I)
  errs() << *I << "\n";

很简单, 不是吗?您还可以使用 InstIterator 将其初始内容填充到工作列表中. 例如, 如果您想要初始化一个工作列表, 其中包含 Function F 中的所有指令, 您只需要执行以下操作:

std::set<Instruction*> worklist;
// 或者更好的选择, SmallPtrSet<Instruction*, 64> worklist;

for (inst_iterator I = inst_begin(F), E = inst_end(F); I != E; ++I)
  worklist.insert(&*I);

现在, STL set 工作列表将包含指向 F 指向的 Function 中的所有指令.

6.1.4 Turning an iterator into a class pointer (and vice-versa)

有时, 当您手头只有迭代器时, 从迭代器中提取类实例的引用(或指针)会很有用. 从迭代器中提取引用或指针非常简单. 假设 i 是 BasicBlock::iterator, j 是 BasicBlock::const_iterator:

Instruction& inst = *i;   // 提取指令引用
Instruction* pinst = &*i; // 提取指令指针
const Instruction& inst = *j;

然而, 在 LLVM 框架中, 您将使用的迭代器是特殊的:它们会在需要时自动转换为指向实例的指针类型. 您可以直接将迭代器分配给正确的指针类型, 无需对迭代器进行解引用和取地址操作(在幕后, 这是通过重载类型转换机制实现的). 因此, 最后一个示例中的第二行:

Instruction *pinst = &*i;

在语义上等同于:

Instruction *pinst = i;

还可以将类指针转换为相应的迭代器, 这是一个常数时间操作(非常高效). 以下代码片段演示了 LLVM 迭代器提供的转换构造函数的用法. 通过使用这些构造函数, 您可以明确地获取某个对象的迭代器, 而无需通过对某个结构进行迭代来获取它:

void printNextInstruction(Instruction* inst) {
  BasicBlock::iterator it(inst);
  ++it; // 在此行之后, it引用inst后面的指令
  if (it != inst->getParent()->end()) errs() << *it << "\n";
}

不幸的是, 这些隐式转换会带来一些代价;它们阻止了这些迭代器符合标准迭代器约定, 因此无法与标准算法和容器一起使用. 例如, 它们会阻止以下代码(其中B是BasicBlock)的编译:

llvm::SmallVector<llvm::Instruction *, 16>(B->begin(), B->end());

因此, 这些隐式转换可能在将来被删除, 并且 operator* 可能会更改为返回指针而不是引用.

6.1.5 Finding call sites: a slightly more complex example

假设您正在编写一个 FunctionPass, 并希望计算整个模块(即在每个 Function 中)中已经在作用域中的某个函数(即一些Function*)的所有位置. 正如您将在后面学到的那样, 您可能希望使用 InstVisitor 以更简单的方式实现此目标, 但本示例将让我们了解如果没有 InstVisitor , 您将如何实现. 以下是我们要完成的伪代码:

将 callCounter 初始化为零
对于模块中的每个函数 f
  对于函数 f 中的每个基本块 b
    对于基本块 b 中的每个指令i
      如果( i 是一个调用并且调用了给定的函数)
        增加 callCounter

以下是实际的代码(请记住, 因为我们正在编写 FunctionPass, 我们的FunctionPass 派生类只需要覆盖 runOnFunction方法):

Function* targetFunc = ...;
class OurFunctionPass : public FunctionPass {
  public:
    OurFunctionPass() : callCounter(0) { }

    bool runOnFunction(Function& F) override {
      for (BasicBlock &B : F) {
        for (Instruction &I : B) {
          if (auto *CB = dyn_cast<CallBase>(&I)) {
            // 我们知道我们遇到了某种调用指令(call、invoke或callbr), 因此我们需要确定它是否是对 m_func 指向的函数的调用. 
            if (CB->getCalledFunction() == targetFunc)
              ++callCounter;
          }
        }
      }
      return false; // 此示例中未进行实际修改
    }
  private:
    unsigned callCounter;
};

以上代码中, 我们假设您已经定义了目标函数 targetFunc. OurFunctionPass 是一个继承自 FunctionPass 的类, 它重写了 runOnFunction 方法. 在该方法中, 我们遍历了函数中的基本块和指令, 并通过 dyn_cast<CallBase> 将指令转换为调用指令类型. 然后, 我们检查调用的函数是否等于 targetFunc , 如果是, 则增加 callCounter 的计数. 最后, 我们将返回值设置为 false, 表示在此示例中未进行实际修改.

6.1.6 Iterating over def-use & use-def chains

经常情况下, 我们可能有一个 Value 类的实例, 并且想要确定哪些 User 使用了该Value. 特定 Value 的所有 User 的列表称为 def-use 链. 例如, 假设我们有一个指向特定函数 foo 的 Function*, 名为 F. 查找使用 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 类的实例, 并且需要知道它使用的哪些 Value. User 使用的所有 Value 的列表称为 use-def 链. Instruction 类的实例通常是User, 因此我们可能希望遍历特定指令使用的所有值(即特定指令的操作数):

Instruction *pi = ...;

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

将对象声明为 const 是强制执行无变异算法(例如分析等)的重要工具. 为此, 上述迭代器具有常量版本, 即 Value::const_use_iterator 和 Value::const_op_iterator. 当在 const Value 或 const User 上调用 use/op_begin() 时, 它们会自动出现. 在解引用时, 它们返回 const Use*. 除此之外, 上述模式保持不变.

6.1.7 Iterating over predecessors & successors of blocks

使用在"llvm/IR/CFG.h"中定义的例程来迭代块的前驱和后继非常简单. 只需使用以下代码来迭代BB的所有前驱:

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

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

类似地, 要迭代后继, 请使用 successors.

6.2 Making simple changes

LLVM基础架构中存在一些原始的转换操作, 值得了解. 在进行转换时, 通常会操作基本块的内容. 本节介绍了一些常见的方法, 并提供了示例代码.

6.2.1 Creating and inserting new Instructions

实例化指令

创建指令非常简单:只需调用要实例化的指令类型的构造函数并提供必要的参数. 例如, AllocaInst仅需要一个(const-ptr-to)Type. 因此:

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

将创建一个 AllocaInst 实例, 表示在运行时当前堆栈帧中分配一个整数. 每个Instruction 子类可能具有不同的默认参数, 可以改变指令的语义, 因此请参阅您要实例化的 Instruction 子类的 doxygen 文档.

命名值

在可能的情况下, 为指令的值命名非常有用, 因为这有助于调试转换过程. 如果您查看生成的 LLVM 机器代码, 肯定希望将逻辑名称与指令的结果关联起来!通过为Instruction 构造函数的 Name(默认)参数提供一个值, 您可以将逻辑名称与指令在运行时执行的结果关联起来. 例如, 假设我正在编写一个动态分配堆栈上整数空间的转换, 该整数将由其他代码用作某种索引. 为此, 我在某个函数的第一个基本块的第一个点放置了一个 AllocaInst, 并且打算在同一函数中使用它. 我可以这样做:

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

其中 indexLoc 现在是指令执行值的逻辑名称, 它是运行时堆栈上整数的指针.

插入指令

将指令插入到形成 BasicBlock 的现有指令序列中基本上有三种方法:

  1. 插入到BasicBlock的指令列表中

给定 BasicBlock* pb, 该 BasicBlock 中的 Instruction* pi, 以及要在*pi之前插入的新创建的指令, 可以执行以下操作:

BasicBlock *pb = ...;
Instruction *pi = ...;
auto *newInst = new Instruction(...);

newInst->insertBefore(pi); // 在pi之前插入newInst

追加到cBasicBlockc的末尾非常常见, 因此cInstructionc类和cInstructionc派生类提供了构造函数, 它们接受指向要追加到的cBasicBlockc的指针. 例如, 下面的代码:

BasicBlock *pb = ...;
auto *newInst = new Instruction(...);

newInst->insertInto(pb, pb->end()); // 将newInst追加到pb

可以改写为:

BasicBlock *pb = ...;
auto *newInst = new Instruction(..., pb);

这样做更加简洁, 特别是如果您正在创建长的指令流.

  1. 使用 IRBuilder 进行插入

使用前面的方法插入多个指令可能相当繁琐. IRBuilder 是一个方便的类, 可用于在 BasicBlock 的末尾或特定指令之前添加多个指令. 它还支持常量折叠和重命名命名寄存器(请参阅 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 Tutorial.

6.2.2 Deleting Instructions

从形成基本块的现有指令序列中删除指令非常简单:只需调用指令的eraseFromParent()方法. 例如:

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

这将将指令从其包含的基本块中取消链接并删除它. 如果您只想将指令从其包含的基本块中取消链接而不删除它, 可以使用 removeFromParent()方法.

6.2.3 Replacing an Instruction with another Value

6.2.3.1 Replacing individual instructions

包含 “llvm/Transforms/Utils/BasicBlockUtils.h” 允许使用两个非常有用的替换函数: ReplaceInstWithValue 和 ReplaceInstWithInst

6.2.3.2 Deleting Instructions
  • 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"));
6.2.3.3 Replacing multiple uses of Users and Values

你可以使用 Value::replaceAllUsesWith 和 User::replaceUsesOfWith 来同时更改多个使用. 有关详细信息, 请参阅分别针对 Value类User类 的Doxygen文档.

6.2.4 Deleting GlobalVariables

从模块中删除全局变量与删除指令一样简单. 首先, 您必须拥有一个指向要删除的全局变量的指针. 然后, 您可以使用该指针将其从其父对象(即模块)中擦除. 例如:

GlobalVariable *GV = .. ;
GV->eraseFromParent();

7 Threads and LLVM

这个部分描述了 LLVM API 与多线程的交互, 包括客户应用程序的部分和 JIT 中的托管应用程序.

需要注意的是, LLVM 对多线程的支持仍然相对较新. 在2.5版本之前, 支持线程化的托管应用程序的执行, 但不支持客户端对API的多线程访问. 虽然现在支持了这种用例, 但客户端必须遵循下面指定的准则, 以确保在多线程模式下正常运行.

需要注意的是, 在类 Unix 平台上, LLVM 需要 GCC 的原子内置函数才能支持多线程操作. 如果在没有适当现代系统编译器的平台上需要一个支持多线程的 LLVM 版本, 可以考虑以单线程模式编译 LLVM 和 LLVM-GCC, 并使用生成的编译器构建支持多线程的 LLVM 副本.

7.1 Ending Execution with llvm_shutdown()

当你使用完LLVM API后, 应调用 llvm_shutdown() 来释放用于内部结构的内存.

7.2 Lazy Initialization with ManagedStatic

ManagedStatic 是 LLVM 中使用的实用类, 用于实现静态资源的静态初始化, 例如全局类型表. 在单线程环境下, 它实现了简单的延迟初始化方案. 然而, 当 LLVM 编译时支持多线程时, 它使用双重检查锁定来实现线程安全的延迟初始化.

7.3 Achieving Isolation with LLVMContext

LLVMContext 是 LLVM API 中的一个不透明类, 客户端可以使用它在同一地址空间内同时操作多个独立的 LLVM 实例. 例如, 在一个假设的编译服务器中, 每个翻译单元的编译在概念上是独立于其他所有翻译单元的, 并且希望能够在独立的服务器线程上并发地编译传入的翻译单元. 幸运的是, LLVMContext存在就是为了实现这种情况!

从概念上讲, LLVMContext 提供了隔离. LLVM 内存中的每个实体(模块、值、类型、常量等)都属于一个 LLVMContext. 不同上下文中的实体之间无法相互交互:不同上下文中的模块无法链接在一起, 函数无法添加到不同上下文的模块中, 等等. 这意味着只要没有两个线程在同一上下文中操作实体, 就可以同时在多个线程上安全地进行编译.

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

7.4 Threads and the JIT

LLVM 的 “eager” 即时编译器在多线程程序中是安全的. 多个线程可以同时调用 ExecutionEngine::getPointerToFunction() 或 ExecutionEngine::runFunction() , 并且多个线程可以同时运行 JIT 生成的代码. 用户仍然必须确保在给定的 LLVMContext 中只有一个线程访问 IR, 而另一个线程可能正在修改它. 一种做法是在访问 JIT 之外的 IR 时始终持有 JIT 锁(JIT通过添加 CallbackVHs 来修改 IR). 另一种方法是只从 LLVMContext 的线程中调用 getPointerToFunction().

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

8 Advanced Topics

本节介绍了一些高级或晦涩的 API, 大多数客户端不需要了解. 这些 API 用于管理 LLVM 系统的内部工作, 并且只在特殊情况下需要访问.

8.1 The ValueSymbolTable class

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

请注意, 大多数客户端不应直接访问 SymbolTable 类. 它仅在需要对符号表名称进行迭代的情况下使用, 这是非常特殊的用途. 请注意, 并非所有的 LLVM 值都有名称, 没有名称的值(即名称为空的值)不会存在于符号表中.

符号表支持使用 begin/end/iterator 对符号表中的值进行迭代, 并支持查询特定名称是否在符号表中(使用lookup方法). ValueSymbolTable 类没有公开的修改器方法, 而是在值上调用 setName, 它会自动将其插入到适当的符号表中.

8.2 The User and owned Use classes’ memory layout

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

8.2.1 Interaction and relationship between User and Use objects

User(子类)可以选择在其内部包含 Use 对象, 或者通过指针引用它们. 混合变体(一些Use对象内联, 其他的挂在外部)是不可行的, 并且违反了同一 User 对象的 Use 对象形成连续数组的不变性.

我们在User(子)类中有两种不同的布局:

  • 布局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 成员中的对象 Use::Prev )

8.3 Designing Type Hierarchies and Polymorphic Interfaces(设计类型层次结构和多态接口)

有两种不同的设计模式在 C++ 程序中常常导致 使用虚函数调度 来处理 类型层次结构中的方法. 第一种是真正的 类型层次结构, 其中层次结构中的不同类型 模拟了特定功能 和 语义的子集, 并且这些类型严格嵌套在彼此之内. Value 或 Type 类型层次结构中可以看到很好的例子.

第二种情况是希望 动态调度一组多态接口实现. 这种后一种用例可以通过定义一个所有实现都从中派生并重写的抽象接口基类来使用虚函数调度和继承来建模. 然而, 这种实现策略强制存在一个实际上并不有意义的 “is-a” 关系. 通常不存在一些有用的泛化嵌套层次结构, 代码可以与之交互并上下移动. 相反, 存在一个唯一的接口, 它在一系列实现之间进行调度.

对于第二种用例, 首选的实现策略是 通用编程(有时称为“编译时鸭子类型”或“静态多态性”). 例如, 可以针对某个类型参数T实例化模板, 该类型参数与符合接口或概念的任何特定实现相一致. 一个很好的例子是任何模拟有向图中节点的类型的高度通用属性. LLVM 主要通过 模板和通用编程 来建模这些. 这些模板包括 LoopInfoBaseDominatorTreeBase. 当这种类型的多态性真正需要动态调度时, 可以使用称为 基于概念的多态性 的技术进行泛化. 该模式使用非常有限的 虚函数调度 来进行类型擦除, 并模拟模板的接口和行为. 在 PassManager.h 系统中可以找到此技术的示例, 并且 Sean Parent 在他的几次演讲和论文中对此进行了更详细的介绍:

在选择 创建类型层次结构(使用标记或虚拟调度)和 使用模板基于概念的多态性 之间时, 请考虑是否存在某个抽象基类的细化, 在接口边界上它是一个语义上有意义的类型. 如果除了根抽象接口以外的任何更具体的内容在语义模型的部分扩展中是没有意义的, 那么您的用例可能更适合使用多态性, 并且应避免使用虚拟调度. 然而, 可能会有一些紧急情况需要使用其中一种技术.

如果确实需要引入类型层次结构, 我们更倾向于使用 显式封闭的类型层次结构, 配合手动标记调度 and/or RTTI, 而不是在 C++ 代码中更常见的 开放继承模型虚拟调度. 这是因为 LLVM 很少鼓励库的使用者扩展其核心类型, 并且利用了其 层次结构的封闭性 和 标记调度 的特性来生成更高效的代码. 我们还发现, 我们大部分对类型层次结构的使用更适合 基于标记的模式匹配, 而不是在共同接口上进行动态调度. 在 LLVM 中, 我们构建了自定义助手来促进这种设计. 请参阅本文档关于 isa 和 dyn_cast 的部分, 以及我们的详细文档, 描述了如何实现此模式以与 LLVM 助手一起使用.

8.4 ABI Breaking Checks

对改变 LLVM C++ ABI 的检查和断言是基于预处理符号 LLVM_ENABLE_ABI_BREAKING_CHECKS 的条件 - 使用 LLVM_ENABLE_ABI_BREAKING_CHECKS 构建的 LLVM 库与未定义该符号的构建的 LLVM 库不兼容. 默认情况下, 打开断言也会打开 LLVM_ENABLE_ABI_BREAKING_CHECKS, 因此默认的 +Asserts 构建与默认的 -Asserts 构建不兼容. 希望在 +Asserts 和 -Asserts 构建之间具有 ABI 兼容性的客户端应该使用 CMake 构建系统来独立设置 LLVM_ENABLE_ABI_BREAKING_CHECKS, 而不是使用 LLVM_ENABLE_ASSERTIONS.

9 The Core LLVM Class Hierarchy Reference

#include “llvm/IR/Type.h”

头文件来源: Type.h

Doxygen 信息: 类型类

核心 LLVM 类是表示要检查或转换的程序的主要手段. 核心 LLVM 类定义在 include/llvm/IR 目录下的头文件中, 实现在 lib/IR 目录中. 值得注意的是, 出于历史原因, 该库被称为 libLLVMCore.so, 而不是像你预期的那样叫做 libLLVMIR.so.

9.1 Type 类和派生类型

Type 是所有类型类的超类. 每个 Value 都有一个 Type. Type 不能直接实例化, 而是通过其子类来实例化. 某些原始类型(VoidType、LabelType、FloatType 和 DoubleType)有隐藏的子类. 它们被隐藏是因为除了与 Type 类提供的功能相同以外, 它们没有其他有用的功能, 只是为了与 Type 的其他子类区分开来.

所有其他类型都是 DerivedType 的子类. 类型可以有名称, 但这不是必需的. 在任何时候, 一个给定形状的类型只有一个实例. 这允许通过 Type 实例的地址相等性执行类型的相等性. 也就是说, 给定两个 Type* 值, 如果指针相等, 则类型相等.

9.1.1 重要的公共方法

  • bool isIntegerTy() const:对于任何整数类型, 返回 true.
  • bool isFloatingPointTy():如果是五种浮点类型之一, 则返回 true.
  • bool isSized():如果类型的大小已知, 则返回 true. 没有大小的东西是抽象类型、标签和 void.

9.1.2 重要的派生类型

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:返回形式参数的数量.

9.2 Module 类

#include “llvm/IR/Module.h”

头文件来源:Module.h

doxygen 信息:Module 类

Module 类代表 LLVM 程序中的顶层结构. 一个 LLVM 模块实际上可以是原始程序的一个翻译单元, 或者是链接器合并的多个翻译单元的组合. Module 类跟踪函数列表、全局变量列表和符号表. 此外, 它还包含一些有用的成员函数, 旨在简化常见操作.

9.2.1 Module 类的重要公共成员

  • Module::Module(std::string name = “”)

构造一个 Module 很简单. 你可以选择为其提供一个名称(可能基于翻译单元的名称).

  • Module::iterator - Function 列表迭代器的类型定义

Module::const_iterator - const_iterator 的类型定义

begin()、end()、size()、empty() 这些是转发方法, 可以方便地访问 Module 对象的 Function 列表内容.

  • Module::FunctionListType &getFunctionList()

返回函数列表. 当需要更新列表或执行没有转发方法的复杂操作时, 需要使用此函数.


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

返回此 Module 的符号表的引用.

  • Function *getFunction(StringRef Name) const

在 Module 的符号表中查找指定的函数. 如果不存在, 则返回空指针.

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

在 Module 的符号表中查找指定的函数. 如果不存在, 则添加该函数的外部声明并返回它. 注意, 已经存在的函数签名可能与请求的签名不匹配. 因此, 为了支持将结果直接传递给 EmitCall 的常见用法, 返回类型是一个结构体 {FunctionType *T, Constant FunctionPtr}, 而不仅仅是可能具有意外签名的 Function.

  • std::string getTypeName(const Type *Ty)

如果指定的 Type 在符号表中至少有一个条目, 则返回该条目. 否则返回空字符串.

  • bool addTypeName(const std::string &Name, const Type *Ty)

在符号表中插入一个映射关系, 将 Name 映射到 Ty. 如果该名称已经存在条目, 则返回 true, 并且符号表不会被修改.

9.3 Value 类

#include “llvm/IR/Value.h”

头文件来源:Value.h

doxygen 信息:Value 类

Value 类是 LLVM 源代码中最重要的类. 它表示一个带类型的值, 可以用作指令的操作数等. Value 有许多不同的类型, 比如 Constants(常量)、Arguments(参数), 甚至 Instructions(指令)和 Functions(函数)也是 Value.

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

因为 LLVM 是一种带类型的表示, 所以每个 LLVM Value 都有类型, 并且可以通过 getType() 方法获取该类型. 此外, 所有 LLVM 值都可以命名. Value 的“名称”是在 LLVM 代码中打印的符号字符串:

%foo = add i32 1, 2

此指令的名称是“foo”. 注意, 任何值的名称都可能丢失(为空字符串), 因此名称只应用于调试(使源代码更易读, 调试打印输出), 不应用于跟踪值或在值之间建立映射. 为此, 请使用指向 Value 自身的指针的 std::map.

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

9.3.1 Value 类的重要公共成员

Value::use_iterator - 用于遍历使用列表的迭代器的类型定义
  • Value::const_use_iterator - 用于遍历使用列表的常量迭代器的类型定义
  • unsigned use_size() - 返回值的使用者数量
  • bool use_empty() - 如果没有使用者则返回 true
  • use_iterator use_begin() - 获取指向使用列表开头的迭代器
  • use_iterator use_end() - 获取指向使用列表末尾的迭代器
  • 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 的使用列表, 将当前值的所有使用者替换为引用“V”. 例如, 如果检测到一条指令始终产生一个常量值(例如通过常量折叠), 可以使用以下方式将该指令的所有使用替换为该常量:

Inst->replaceAllUsesWith(ConstVal);

9.4 User 类

#include “llvm/IR/User.h”

头文件来源:User.h

doxygen 信息:User 类

父类:Value

User 类是所有可能引用值的 LLVM 节点的通用基类. 它公开了一个“操作数”列表, 其中包含用户所引用的所有值. User 类本身是 Value 的子类.

User 的操作数直接指向它所引用的 LLVM 值. 由于 LLVM 使用静态单赋值(SSA)形式, 因此只能引用一个定义, 从而允许进行直接连接. 这种连接提供了 LLVM 中的使用定义信息.

9.4.1 User 类的重要公共成员

User 类以两种方式公开操作数列表:通过索引访问接口和基于迭代器的接口.

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

这两个方法以便于直接访问的形式公开了 User 的操作数.

User::op_iterator - 用于操作数列表的迭代器的 typedef
  • op_iterator op_begin() - 获取指向操作数列表开头的迭代器.
  • op_iterator op_end() - 获取指向操作数列表末尾的迭代器.

这些方法共同构成了基于迭代器的 User 操作数接口

9.5 指令(Instruction)类

#include “llvm/IR/Instruction.h”

头文件来源:Instruction.h

Doxygen 信息:指令类(Instruction Class)

超类:User, Value

指令类是所有 LLVM 指令的共同基类. 它提供了一些方法, 但是是一个非常常用的类. 指令类本身主要跟踪的数据是操作码(指令类型)和包含指令的基本块(BasicBlock)的信息. 为了表示特定类型的指令, 会使用指令类的许多子类之一.

由于指令类是 User 类的子类, 因此可以像处理其他 User 类型一样访问其操作数(使用 getOperand()/getNumOperands() 和 op_begin()/op_end() 方法). Instruction 类的一个重要文件是 llvm/Instruction.def 文件. 该文件包含有关 LLVM 中各种不同类型指令的元数据. 它描述了用作操作码的枚举值(例如 Instruction::Add 和 Instruction::ICmp), 以及实现指令的具体子类(例如 BinaryOperator 和 CmpInst). 不幸的是, 由于该文件中使用了宏, 这些枚举值在 Doxygen 输出中无法正确显示.

9.5.1 指令(Instruction)类的重要子类:

  • BinaryOperator

这个子类代表所有的二元操作指令, 其操作数必须是相同类型(除了比较指令).

  • CastInst

这个子类是 12 条类型转换指令的父类. 它提供了对类型转换指令的常见操作.

  • CmpInst

这个子类代表两个比较指令, 即 ICmpInst(整数操作数)和 FCmpInst(浮点数操作数).

9.5.2 指令类的重要公共成员:

  • BasicBlock *getParent()

返回包含该指令的基本块(BasicBlock).

  • bool mayWriteToMemory()

如果该指令写入内存(即调用、释放、调用函数或存储操作), 则返回 true.

  • unsigned getOpcode()

返回指令的操作码(opcode).

  • Instruction *clone() const

返回一个与原指令在所有方面都相同的指令实例, 唯一的区别是新实例没有父级(即没有嵌入到基本块中)并且没有名称.

9.6 Constant 类及其子类

Constant 类是表示不同类型常量的基类. 它的子类包括 ConstantInt、ConstantArray 等, 用于表示各种类型的常量. GlobalValue 也是一个子类, 用于表示全局变量或函数的地址.

9.6.1 Constant 的重要子类:

  • ConstantInt: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):返回表示 Val 提供的值的 ConstantInt 对象, 其整数类型为 Ty.
  • ConstantFP:这个类表示浮点数常量.
    • double getValue() const:返回该常量的底层值.
  • ConstantArray:这个类表示常量数组.
    • const std::vector &getValues() const:返回由组成此数组的组件常量的向量.
  • ConstantStruct:这个类表示常量结构体.
    • const std::vector &getValues() const:返回由组成此结构体的组件常量的向量.
  • GlobalValue:这个类表示全局变量或函数. 无论哪种情况, 该值都是一个常量的固定地址(链接后).

9.7 GlobalValue 类

#include “llvm/IR/GlobalValue.h”

头文件源代码:GlobalValue.h

doxygen 信息:GlobalValue 类

父类:ConstantUserValue

全局值(GlobalVariables 或 Functions)是唯一可以在所有函数体中可见的 LLVM 值. 由于它们在全局范围内可见, 它们也会与在不同翻译单元中定义的其他全局变量进行链接. 为了控制链接过程, GlobalValues 知道它们的链接规则. 具体来说, GlobalValues 知道它们是否具有内部或外部链接, 这由 LinkageTypes 枚举定义.

如果 GlobalValue 具有内部链接(相当于 C 语言中的 static), 它对当前翻译单元之外的代码不可见, 并且不参与链接. 如果它具有外部链接, 它对外部代码可见, 并参与链接. 除了链接信息, GlobalValues 还跟踪它们当前所属的模块.

因为 GlobalValues 是内存对象, 所以它们总是通过它们的地址引用. 因此, 全局的类型始终是指向其内容的指针. 在使用 GetElementPtrInst 指令时, 记住这一点很重要, 因为首先必须对该指针进行解引用. 例如, 如果你有一个 GlobalVariable(GlobalValue 的子类), 它是一个包含 24 个 int 的数组, 类型为 [24 x i32], 那么 GlobalVariable 是指向该数组的指针. 尽管该数组的第一个元素的地址和 GlobalVariable 的值相同, 但它们具有不同的类型. GlobalVariable 的类型是 [24 x i32], 第一个元素的类型是 i32. 因此, 访问全局值需要首先使用 GetElementPtrInst 解引用指针, 然后才能访问其元素. 这在 LLVM 语言参考手册中有解释.

9.7.1 GlobalValue 类的重要公共成员

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

这些方法用于操作 GlobalValue 的链接特性.

Module *getParent()

该方法返回当前嵌入到的模块(Module)中的 GlobalValue.

9.8 Function 类

#include “llvm/IR/Function.h”

头文件来源: Function.h

doxygen 信息: Function 类

Superclasses: GlobalValue, Constant, User, Value

Function 类表示 LLVM 中的单个过程. 它实际上是 LLVM 层次结构中比较复杂的类之一, 因为它必须跟踪大量的数据. Function 类记录了基本块(BasicBlocks)的列表、形式参数(Arguments)的列表和符号表(SymbolTable).

基本块列表是 Function 对象中最常用的部分. 该列表对函数中的基本块施加了隐式的排序, 指示后端将如何布局代码. 此外, 第一个基本块是函数的隐式入口节点. 在 LLVM 中, 不能显式地跳转到该初始块. 没有隐式的退出节点, 实际上一个函数可能有多个退出节点. 如果基本块列表为空, 这表示该函数实际上是函数声明:函数的实际体尚未链接.

除了基本块列表外, Function 类还跟踪函数接收的形式参数列表. 该容器管理参数节点的生命周期, 就像基本块列表对基本块的管理一样.

符号表是 LLVM 中一个很少使用的特性, 只有在需要通过名称查找值时才会使用. 除此之外, 符号表在内部用于确保函数体中的指令、基本块或参数的名称不会冲突.

请注意, Function 是一个 GlobalValue, 因此也是一个 Constant. 函数的值是其地址(链接后), 保证是常量.

9.8.1 Function 类的重要公共成员

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

当需要创建新的函数并将其添加到程序中时使用的构造函数. 构造函数必须指定要创建的函数的类型以及函数应具有的链接类型. FunctionType 参数指定函数的形式参数和返回值. 可以使用相同的 FunctionType 值创建多个函数. Parent 参数指定函数所在的模块. 如果提供了此参数, 函数将自动插入到该模块的函数列表中.

  • bool isDeclaration()

返回函数是否定义了函数体. 如果函数是“external”(外部函数), 则它没有函数体, 因此必须通过与另一个翻译单元中定义的函数进行链接来解析.

  • Function::iterator - 基本块列表迭代器的类型定义

    Function::const_iterator - 常量迭代器的类型定义

    begin()、end()、size()、empty()、insert()、splice()、erase()
    这些是转发方法, 便于访问 Function 对象的基本块列表的内容.

  • Function::arg_iterator - 形式参数列表迭代器的类型定义

    Function::const_arg_iterator - 常量迭代器的类型定义

    arg_begin()、arg_end()、arg_size()、arg_empty()
    这些是转发方法, 便于访问 Function 对象的形式参数列表的内容.

  • Function::ArgumentListType &getArgumentList()

返回形式参数的列表. 在需要更新列表或执行没有转发方法的复杂操作时, 使用此方法.

  • BasicBlock &getEntryBlock()

返回函数的入口基本块. 由于函数的入口块始终是第一个块, 因此此方法返回 Function 的第一个块.

  • Type *getReturnType()

    FunctionType *getFunctionType()
    遍历函数的类型并返回函数的返回类型, 或者返回实际函数的 FunctionType.

  • SymbolTable *getSymbolTable()

返回该函数的符号表指针.

9.9 GlobalVariable 类

#include “llvm/IR/GlobalVariable.h”

头文件来源:GlobalVariable.h

doxygen 信息:GlobalVariable 类

父类:GlobalValue, Constant, User, Value

全局变量使用 GlobalVariable 类表示(surprise surprise). 与函数一样, GlobalVariable 也是 GlobalValue 的子类, 因此它们始终通过地址引用(全局值必须驻留在内存中, 因此它们的“名称”指的是它们的常量地址). 请参阅 GlobalValue 以了解更多信息. 全局变量可以具有初始值(必须是 Constant 类型), 如果它们具有初始值, 还可以标记为“常量”(表示它们在运行时不会更改).

9.9.1 GlobalVariable 类的重要公共成员

  • GlobalVariable(const Type *Ty, bool isConstant, LinkageTypes &Linkage, Constant Initializer = 0, const std::string &Name = “”, Module Parent = 0)

创建指定类型的新全局变量. 如果 isConstant 为 true, 则全局变量将被标记为程序中不可更改. Linkage 参数指定变量的链接类型(internal、external、weak、linkonce、appending). 如果链接是 InternalLinkage、WeakAnyLinkage、WeakODRLinkage、LinkOnceAnyLinkage 或 LinkOnceODRLinkage, 则生成的全局变量将具有内部链接. AppendingLinkage 将变量的所有实例(在不同的翻译单元中)连接到单个变量中, 但仅适用于数组. 有关链接类型的详细信息, 请参阅 LLVM 语言参考手册. 还可以为全局变量指定初始化器、名称和要放置变量的模块.

  • bool isConstant() const

如果这是一个在运行时不会修改的全局变量, 则返回 true.

  • bool hasInitializer()

如果此 GlobalVariable 具有初始化器, 则返回 true.

  • Constant *getInitializer()

返回 GlobalVariable 的初始值. 如果没有初始化器, 调用此方法是不合法的.

9.10 BasicBlock 类

#include “llvm/IR/BasicBlock.h”

头文件来源:BasicBlock.h

doxygen 信息:BasicBlock 类

父类:Value

该类表示代码中的 单入口单出口区域, 编译器社区通常称之为基本块(basic block). BasicBlock 类维护了一个指令列表, 这些指令构成了基本块的主体. 根据语言定义, 指令列表的最后一个元素始终是终止指令.

除了跟踪构成基本块的指令列表之外, BasicBlock 类还记录了所嵌入的函数.

请注意, 基本块本身是 Value 类的实例, 因为它们被诸如分支指令和 switch 表中的指令引用. 基本块的类型为 label.

9.10.1 BasicBlock 类的重要公共成员

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

基本块构造函数用于创建要插入到函数中的新基本块. 构造函数可选地接受新块的名称和要插入的函数. 如果指定了 Parent 参数, 新的 BasicBlock 会自动插入到指定函数的末尾;如果未指定, 则必须手动将 BasicBlock 插入到函数中.

  • BasicBlock::iterator - 指令列表的迭代器的类型定义

    BasicBlock::const_iterator - const_iterator 的类型定义.

    begin()、end()、front()、back()、size()、empty()、splice() - 用于访问指令列表的 STL 风格函数.

这些方法和类型定义是具有与同名标准库方法相同语义的转发函数. 这些方法以易于操作的方式暴露基本块的底层指令列表.

  • Function *getParent()

返回指向包含该基本块的函数的指针, 如果基本块没有所属函数, 则返回空指针.

  • Instruction *getTerminator()

返回指向出现在 BasicBlock 末尾的终止指令的指针. 如果没有终止指令, 或者块中的最后一条指令不是终止指令, 则返回空指针.

9.11 Argument 类

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值