LLVM开发者手册
- 1. 简介
- 2. 通用信息
- 3. 重要且有用的LLVM APIs
- 4. 为任务选择正确的数据结构
- 4.1 Sequential Containers (std::vector, std::list, etc)
- 4.2 String-like containers
- 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 \
- 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.5 Bit storage containers (BitVector, SparseBitVector)
- 5. 调试
- 6. 常见操作的有用提示
- 7. 线程和LLVM
- 8. 高级的主题
- 9. 核心LLVM类层次结构参考资料
说明:本文为译文,点击 此处查看原文。
1. 简介
本文档旨在突出显示LLVM源代码库中可用的一些重要类和接口。本手册不打算解释什么是LLVM,它是如何工作的,以及LLVM代码是什么样子的。它假设您了解LLVM的基本知识,并且对编写转换或其他分析或操作代码感兴趣。
这个文档应该使您面向对象,这样您就可以在组成LLVM基础架构的不断增长的源代码中找到自己的方法。请注意,本手册不打算替代阅读源代码,所以如果您认为这些类中应该有一个方法来做某些事情,但是没有列出,请检查源代码。本文提供了到doxygen源的链接,使这尽可能容易理解。
本文档的第一部分描述了在LLVM基础架构中工作时需要了解的一般信息,第二部分描述了核心LLVM类。在未来,本手册将扩展描述如何使用扩展库的信息,如dominator信息、CFG遍历例程和InstVisitor (doxygen)模板等有用的实用程序。
2. 通用信息
本节包含一些通用信息,如果您在LLVM源代码库中工作,这些信息非常有用,但是这些信息并不特定于任何特定的API。
2.1 C++标准模板库
LLVM大量使用了C++标准模板库(STL),这可能比您以前使用或见过的要多得多。因此,您可能想要对库中使用的技术和功能做一些背景阅读。有很多讨论STL的不错的页面,您可以找到一些关于这个主题的书籍,因此在本文中不会讨论它。
以下是一些有用的链接:
- cppreference.com —— 对于STL和标准C++库的其他部分是一个很好的参考。
- C++ In a Nutshell —— 简而言之,这是一本正在制作中的O’Reilly的书。它有一个相当不错的标准库参考资料,可以和Dinkumware的媲美,但不幸的是,自从这本书出版以来,它就不再是免费的了。
- C++常见问题。
- SGI’s STL程序员指南 —— 包含一个有用的STL介绍。
- Bjarne Stroustrup’s C++ Page。
- Bruce Eckel的《Thinking in C++》,第二版,第二卷,修订版4.0。
还鼓励您阅读LLVM编码标准指南,该指南侧重于如何编写可维护的代码,而不是简单的介绍花括号放在哪里。
2.2 其他有用的参考资料
3. 重要且有用的LLVM APIs
在这里,我们重点介绍了一些LLVM APIs,它们通常很有用,在编写转换时很容易了解它们。
3.1 isa<>、cast<> 和 dyn_cast<> 模板
LLVM源代码库广泛使用了RTTI的自定义形式。这些模板与c++ dynamic_cast<>操作符有很多相似之处,但是它们没有一些缺点(主要是因为 dynamic_cast<> 只适用于具有一个v-table的类)。因为它们经常被使用,所以您必须知道它们用来做什么以及如何工作。所有这些模板都是在 llvm/Support/Casting.h(doxygen) 文件中定义的(注意,很少需要直接包含该文件)。
- isa<>:
isa<>
操作符的工作原理与Java “instanceof”操作符完全相同。它返回true或false,这取决于引用或指针是否指向指定类的实例。这对于各种类型的约束检查非常有用(下面的示例)。 - cast<>:
cast<>
操作符是一个“已检查的强制转换”操作。它将指针或引用从基类转换为派生类,如果不是正确类型的实例,则会导致断言失败。当你有一些信息使你相信某样东西是正确的类型时,应该使用这种方法。isa<> 和 cast<> 模板的一个例子是:
注意,您不应该使用 isa<> 测试后面紧跟着一个 cast<>,因为它使用 dyn_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()); }
- dyn_cast<>:
dyn_cast<>
操作符是一个“检查转换”操作。它检查操作数是否属于指定的类型,如果是,则返回指向它的指针(该操作符不与引用一起工作)。如果操作数类型不正确,则返回空指针。因此,这与c++中的dynamic_cast<>操作符非常相似,应该在相同的环境中使用。通常,dyn_cast<>操作符用于if语句或其他类似的流控制语句中:
这种形式的if语句有效地将对 isa<> 的调用和对 cast<> 的调用组合到一个语句中,这非常方便。if (auto *AI = dyn_cast<AllocationInst>(Val)) { // ... }
注意,dyn_cast<>操作符可以被滥用,就像c++的dynamic_cast<>或Java的instanceof操作符一样。特别是,不应该使用大的if/then/else块来检查类的许多不同变体。如果您发现自己想这样做,那么使用 InstVisitor 类直接分派指令类型会更清晰、更有效。 - cast_or_null<>:
cast_or_null<>
操作符的工作原理与 cast<>操作符类似,只是它允许一个空指针作为参数(然后将其传播)。这有时很有用,允许您将多个null检查合并到一个检查中。 - dyn_cast_or_null<>:
dyn_cast_or_null<>
操作符的工作原理与 dyn_cast<> 操作符类似,只是它允许一个空指针作为参数(然后将其传播)。这有时很有用,允许您将多个null检查合并到一个检查中。
这五个模板可以用于任何类,无论它们是否具有一个v-table。如果希望添加对这些模板的支持,请参阅如何为类层次结构设置LLVM样式的RTTI的文档
3.2 传递字符串(StringRef和Twine类)
虽然LLVM通常不做太多字符串操作,但是我们有几个重要的APIs接受字符串。两个重要的例子是 Value
类(它有指令、函数等的名称)和 StringMap
类(在 LLVM 和 Clang 中广泛使用)。
这些是泛型类,它们需要能够接受可能包含空字符的字符串。因此,它们不能简单地接受const char *
,而接受const std::string&
要求客户机执行堆分配,这通常是不必要的。代替的是,许多LLVM APIs使用StringRef
或const twine&
来有效地传递字符串。
3.2.1 StringRef类
StringRef
数据类型表示对常量字符串(一个字符数组和一个长度)的引用,并支持std::string
上可用的公共操作,但不需要堆分配。
它可以使用一个C风格的以null结尾的字符串、一个std::string
隐式地被造,也可以使用一个字符指针和长度显式地构造。例如,StringRef find
函数声明为:
iterator find(StringRef Key);
client可以用以下任意一种方式调用这个函数:
Map.find("foo"); // Lookup "foo"
Map.find(std::string("bar")); // Lookup "bar"
Map.find(StringRef("\0baz", 4)); // Lookup "\0baz"
类似地,需要返回string
的APIs可能会返回一个StringRef
实例,该实例可以直接使用,也可以使用str
成员函数将其转换为std::string
。有关更多信息,请查看 llvm/ADT/StringRef.h (doxygen)。
您应该很少直接使用StringRef
类,因为它包含指向外部内存的指针,所以存储该类的实例通常是不安全的(除非您知道不会释放外部存储)。StringRef
在 LLVM 中足够小和普遍,因此它应该总是通过值传递。
3.2.2 Twine类
Twine
(doxygen)类是 APIs 接受连接字符串的有效方法。例如,一个常见的LLVM范型是根据带有后缀的另一条指令的名称来命名一条指令,例如:
New = CmpInst::Create(..., SO->getName() + ".cmp");
Twine
类实际上是一个轻量级的rope,它指向临时(分配给栈的)对象。Twine
可以隐式地构造为加运算符应用于字符串的结果(即,一个C字符串,一个std::string
,或者一个StringRef
)。Twine
会延迟字符串的实际连接,直到实际需要它时,才会有效地将其直接呈现到字符数组中。这避免了在构造字符串连接的临时结果时涉及的不必要的堆分配。有关更多信息,请查看 llvm/ADT/Twine.h(doxygen)和这里。
与StringRef
一样,Twine
对象指向外部内存,并且几乎不应该直接存储或提及。它们仅用于在定义一个应该能够有效接受连接字符串的函数时使用。
3.3 格式化字符串(formatv函数)
虽然LLVM不一定要做很多字符串操作和解析,但它确实做了很多字符串格式化。从诊断消息,到llvm工具输出(如llvm-readobj
),再到打印详细的分解清单和LLDB运行时日志,字符串格式化的需求无处不在。
formatv
在本质上类似于printf
,但是使用了另一种语法,这种语法大量借鉴了Python和c#。与printf不同,它推断要在编译时格式化的类型,因此不需要%d之类的格式说明符。这减少了构造可移植格式字符串的脑力开销,特别是对于size_t或指针类型等特定于平台的类型。与printf和Python不同的是,如果LLVM不知道如何格式化类型,它还不能编译。这两个属性确保函数比传统的格式化方法(如printf函数族)更安全,使用起来也更简单。
3.3.1 简单的格式化
formatv
调用涉及一个由0个或多个替换序列
组成的格式字符串,然后是替换值
的一个可变长度列表。一个替换序列是一个形式为{N[[,align]:style]}
的字符串。
N表示替换值列表中参数的基于0的索引。注意,这意味着可以以任何顺序多次引用相同的参数,可能使用不同的样式和/或对齐选项。
align是一个可选字符串,指定要将值格式化为的字段的宽度,以及字段内值的对齐方式。它被指定为一个可选的对齐样式
,后跟一个正整数字段宽度
。对齐样式可以是字符-(左对齐)、=(中对齐)或+(右对齐)
中的一个。默认值是右对齐的。
style是一个可选字符串,由控制值格式的特定类型组成。例如,要将浮点值格式化为百分比,可以使用样式选项P。
3.3.2 自定义格式化
有两种方法可以定制一个类型的格式化行为。
- 使用适当的静态格式化方法为您的类型T提供
llvm::format_provider<T>
的模板专门化。
这是一个有用的扩展机制,用于添加对使用自定义样式选项格式化自定义类型的支持。但是,当您想要扩展格式化库已经知道如何格式化的类型的机制时,它没有帮助。为此,我们需要别的东西。namespace llvm { template<> struct format_provider<MyFooBar> { static void format(const MyFooBar &V, raw_ostream &Stream, StringRef Style) { // Do whatever is necessary to format `V` into `Stream` } }; void foo() { MyFooBar X; std::string S = formatv("{0}", X); } }
- 提供从llvm::FormatAdapter继承的格式适配器。
如果检测到该类型派生自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
方法。这允许提供任何类型的自定义格式,包括已经有内置格式提供程序的格式。
3.3.3 formatv例子
下面将提供一组不完整的示例,演示formatv
的用法。通过阅读doxygen文档或查看单元测试套件可以找到更多信息。
std::string S;
// 基本类型的简单格式化和隐式字符串转换。
S = formatv("{0} ({1:P})", 7, 0.35); // S == "7 (35.00%)"
// 无序引用和多引用
outs() << formatv("{0} {2} {1} {0}", 1, "test", 3); // prints "1 3 test 1"
// 左、右、中对齐
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";
// 自定义样式
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契约的违反,并表示程序本身中的错误。我们的目标是记录不变量,并在不变量在运行时被破坏时在故障点快速中止(提供一些基本的诊断)。
处理编程错误的基本工具是断言
和llvm_unaccessible
函数。断言用于表示不变条件,并且应该包含描述不变条件的消息:
assert(isPhysReg(R) && "All virt regs should have been allocated already.");
llvm_unaccessible
函数可用于记录控制流的区域,如果程序不变量保持:
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");
}
3.4.2 可恢复性错误
可恢复错误表示程序环境中的错误,例如资源故障(丢失的文件、丢失的网络连接等)或格式错误的输入。应该检测这些错误,并将其传达给程序的某个级别,以便对其进行适当处理。处理错误可能与向用户报告问题一样简单,也可能涉及尝试恢复。
请注意
虽然在整个LLVM中使用这种错误处理方案是理想的,但是在某些地方这种方法并不实用。在绝对必须发出非编程错误且错误模型不可用的情况下,可以调用report_fatal_error,它将调用已安装的错误处理程序、打印一条消息并退出程序。
可恢复错误使用LLVM的错误模式建模。这个方案使用函数返回值表示错误,类似于经典的C整数错误代码,或者c++的std::error_code。然而,Error类实际上是用户定义错误类型的轻量级包装器,允许附加任意信息来描述错误。这类似于c++异常允许抛出用户定义类型的方式。
成功值是通过调用Error:: Success()创建的,例如:
Error foo() {
// Do something.
// Return success.
return Error::success();
}
成功值的构建和返回非常便宜——它们对程序性能的影响很小。
使用make_error构造失败值,其中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();
}
错误值可以隐式地转换为bool: true for Error, false for success
,启用以下习语:
Error mayFail();
Error foo() {
if (auto Err = mayFail())
return Err;
// Success! We can proceed.
...
对于可能失败但需要返回值的函数,可以使用预期的实用程序。这种类型的值可以用T或错误构造。预期值也可以隐式转换为布尔值,但与错误的约定相反:true表示成功,false表示错误。如果成功,可以通过取消引用操作符访问T值。如果失败,可以使用takeError()方法提取错误值。习惯用法如下:
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值处于成功模式,则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;
...
}
对于包含多个预期值的函数,第二种形式通常更具可读性,因为它限制了所需的缩进。
所有错误实例,无论是成功还是失败,都必须在销毁之前进行检查或从(通过std::move或return)移动(通过std::move或return)。意外丢弃未检查的错误将导致程序在未检查值的析构函数运行时中止,从而很容易识别和修复违反此规则的行为。
一旦测试成功值(通过调用布尔转换操作符),就认为它们已被检查:
if (auto Err = mayFail(...))
return Err; // Failure value - move error to caller.
// Safe to continue: Err was checked.
相反,下面的代码总是会导致中止,即使mayFail返回一个成功值:
mayFail();
// Program will always abort here, even if mayFail() returns Success, since
// the value is not checked.
一旦错误类型的处理程序被激活,就会认为检查了故障值:
handleErrors(
processFormattedFile(...),
[](const BadFileFormat &BFF) {
report("Unable to process " + BFF.Path + ": bad format");
},
[](const FileNotFound &FNF) {
report("File not found " + FNF.Path);
});
handleErrors函数将一个错误作为它的第一个参数,然后是一个由“处理程序”组成的可变参数列表,其中每个处理程序必须是一个可调用类型(函数、lambda或带有调用操作符的类)和一个参数。handleErrors函数将访问序列中的每个处理程序,并根据错误的动态类型检查其参数类型,运行第一个匹配的处理程序。这与决定为c++异常运行哪个catch子句所用的决策过程相同。
由于传递给handleErrors的处理程序列表可能不能覆盖所有可能发生的错误类型,因此handleErrors函数还返回一个必须检查或传播的错误值。如果传递给handleErrors的错误值与任何处理程序不匹配,则将从handleErrors返回该值。因此,handleErrors的习惯用法如下:
if (auto Err =
handleErrors(
processFormattedFile(...),
[](const BadFileFormat &BFF) {
report("Unable to process " + BFF.Path + ": bad format");
},
[](const FileNotFound &FNF) {
report("File not found " + FNF.Path);
}))
return Err;
在您真正知道处理程序列表是详尽的情况下,可以使用handleAllErrors函数。这与handleErrors相同,只是如果传入未处理的错误,它将终止程序,因此可以返回void。通常应该避免handleAllErrors函数:在程序的其他地方引入新的错误类型可以很容易地将以前详尽的错误列表转换为非详尽的列表,从而冒着程序意外终止的风险。在可能的情况下,使用handleErrors并将未知的错误传播到堆栈中。
对于工具代码,可以通过打印错误消息然后使用错误代码退出来处理错误,ExitOnError实用程序可能是比handleErrors更好的选择,因为它简化了调用易出错函数时的控制流。
在已知的情况下,一个特定的调用的函数总是成功(例如,调用一个函数,它只能失败的一个子集的输入和输入,是安全的)cantFail函数可以用来删除错误类型,简化控制流。
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(它的作用与Expected相同,但是包装的是std::error_code,而不是错误)。错误类型的传染性意味着,试图更改其中一个函数以返回Error或Expected,结果往往导致对调用者、调用者的调用者的大量更改,等等。(第一次尝试,从MachOObjectFile的构造函数返回一个错误,在diff达到3000行之后被放弃,影响了6个库,并且仍然在增长)。
为了解决这个问题,引入了Error/std::error_code互操作性需求。两对函数允许任何错误值转换为std::error_code,任何期望的转换为ErrorOr,反之亦然:
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更新为预期的。
3.4.2.3 从错误处理程序返回错误
错误恢复尝试本身可能会失败。因此,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函数返回,以便它可以自己处理,或者向上传播堆栈。
3.4.2.4 使用ExitOnError简化工具代码
库代码不应该为可恢复错误调用exit,但是在工具代码中(尤其是命令行工具),这是一种合理的方法。遇到错误时调用exit可以极大地简化控制流,因为不再需要将错误传播到堆栈上。这允许以直线方式编写代码,只要每个容易出错的调用都封装在check中并调用退出即可。ExitOnError类支持这种模式,它提供检查错误值的调用操作符,在成功的情况下清除错误,并将日志记录到stderr,然后在失败的情况下退出。
要使用这个类,请在程序中声明一个全局ExitOnError变量:
ExitOnError ExitOnErr;
对易出错函数的调用可以用对ExitOnErr的调用进行包装,将它们转换为非失败调用:
Error mayFail();
Expected<int> mayFail2();
void foo() {
ExitOnErr(mayFail());
int X = ExitOnErr(mayFail2());
}
失败时,错误的日志消息将被写入stderr,前面可选地加上一个字符串“banner”,可以通过调用setBanner方法设置该字符串。还可以使用setExitCodeMapper方法从错误值映射到退出代码:
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函数封装了这一点,它封装了一个断言,即它们的参数是一个成功值,并且,在预期的情况下,解包T值:
Error onlyFailsForSomeXValues(int X);
Expected<int> onlyFailsForSomeXValues2(int X);
void foo() {
cantFail(onlyFailsForSomeXValues(KnownSafeValue));
int Y = cantFail(onlyFailsForSomeXValues2(KnownSafeValue));
...
}
与ExitOnError实用程序一样,cantFail简化了控制流。但是,它们对错误情况的处理非常不同:当ExitOnError保证在错误输入时终止程序时,cantFile简单地断言结果是成功的。在调试构建中,如果遇到错误,这将导致断言失败。在release构建中,cantFail的行为没有定义失败值。因此,在使用cantFail时必须非常小心:客户端必须确保cantFail包装的调用确实不能因为给定的参数而失败。
cantFail函数在库代码中应该很少使用,但是它们更可能用于工具和单元测试代码中,在这些代码中,输入和/或模拟的类或函数可能是安全的。
3.4.2.6 的构造函数
有些类需要资源获取或其他复杂的初始化,在构建过程中可能会失败。不幸的是,构造函数不能返回错误,而在构造完客户端测试对象以确保它们是有效的之后,让客户端测试对象很容易出错,因为很容易忘记测试。要解决这个问题,使用命名构造函数习惯用法并返回一个期望的:
class Foo {
public:
static Expected<Foo> Create(Resource R1, Resource R2) {
Error Err;
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();
}
};
在这里,指定的构造函数通过引用将错误传递给实际的构造函数,然后构造函数可以使用该构造函数返回错误。ErrorAsOutParameter实用程序在进入构造函数时设置错误值的checked标志,以便将错误分配给构造函数,然后在退出时重置该标志,以强制客户机(指定的构造函数)检查错误。
通过使用这个习惯用法,试图构造Foo的客户端要么接收格式良好的Foo,要么接收错误,而不是处于无效状态的对象。
3.4.2.7 根据类型传播和消耗错误
在某些上下文中,某些类型的错误被认为是良性的。例如,在遍历归档文件时,一些客户机可能乐于跳过格式糟糕的目标文件,而不是立即终止遍历。可以使用一种精心设计的处理程序方法来跳过格式糟糕的对象,但是Error.h头提供了两个实用程序,使这个习惯用法更加简洁:类型检查方法isA和consumeError函数:
Error walkArchive(Archive A) {
for (unsigned I = 0; I != A.numMembers(); ++I) {
auto ChildOrErr = A.getMember(I);
if (auto Err = ChildOrErr.takeError()) {
if (Err.isA<BadFileFormat>())
consumeError(std::move(Err))
else
return Err;
}
auto &Child = *ChildOrErr;
// Use Child
...
}
return Error::success();
}
``
###### 3.4.2.8 连接错误和joinErrors
在上面的归档行走示例中,BadFileFormat错误被简单地使用和忽略。如果客户想在完成归档后报告这些错误,他们可以使用joinErrors实用工具:
```cpp
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 构建容易出错的迭代器和迭代器范围
上面的归档行走示例按索引检索归档成员,但是这需要相当多的样板文件来进行迭代和错误检查。我们可以使用“容易出错的迭代器”模式来清理这个问题,该模式支持对容易出错的容器(如存档)使用以下自然迭代习语:
Error Err;
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实用程序对这种易出错迭代器接口的实例进行包装,该实用程序提供了操作符++和操作符——,通过在构建时传递给包装器的引用返回任何错误。fallible_iterator包装负责(a)跳在误差范围的结束,和(b)标记错误检查每次迭代器相比,发现是不平等的(特别是:这标志着错误的全身检查基于范围for循环),使早期退出循环,没有多余的错误检查。
错误迭代器接口的实例(例如上面的错误迭代器)使用make_fallible_itr和make_fallible_end函数进行包装。例如:
class Archive {
public:
using child_iterator = fallible_iterator<FallibleChildIterator>;
child_iterator child_begin(Error &Err) {
return make_fallible_itr(FallibleChildIterator(*this, 0), Err);
}
child_iterator child_end() {
return make_fallible_end(FallibleChildIterator(*this, size()));
}
iterator_range<child_iterator> children(Error &Err) {
return make_range(child_begin(Err), child_end());
}
};
使用fallible_iterator实用程序允许自然构造容易出错的迭代器(使用失败的inc和dec操作)和相对自然地使用c++迭代器/循环习惯用法
有关错误及其相关实用程序的更多信息可以在Error.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 function_ref类模板
function_ref (doxygen)类模板表示对可调用对象的引用,并在可调用对象的类型上进行模板化。如果在函数返回后不需要保留回调,那么这是向函数传递回调的好选择。这样,function_ref与std::函数的关系就像StringRef与std::string的关系一样。
function_ref<ret(param1, param2,…<="" span=""></ret(param1,>
例如:
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_ref足够小,它应该总是通过值传递。
3.6 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()宏而不是自制的解决方案,允许您不必为您的传递的调试输出创建“另一个”命令行选项。注意,LLVM_DEBUG()宏对于非断言构建是禁用的,因此它们根本不会造成性能影响(出于同样的原因,它们也不应该包含副作用!)
LLVM_DEBUG()宏的另一个好处是,您可以在gdb中直接启用或禁用它。如果程序正在运行,只需从gdb中使用“set DebugFlag=0”或“set DebugFlag=1”。如果程序还没有启动,您总是可以使用-debug运行它。
3.6.1 带有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- optimization)中不可用。
DEBUG_WITH_TYPE宏也适用于您想要设置DEBUG_TYPE的情况,但只适用于一个特定的调试语句。它接受一个附加的第一个参数,即要使用的类型。例如,上面的例子可以写成:
DEBUG_WITH_TYPE("foo", dbgs() << "'foo' debug type\n");
DEBUG_WITH_TYPE("bar", dbgs() << "'bar' debug type\n");
3.7 Statistic类& -stats选项
llvm/ADT/ statistics .h (doxygen)文件提供了一个名为statistics的类,它被用作一种统一的方法来跟踪llvm编译器在做什么以及各种优化的有效性。了解哪些优化有助于加快特定程序的运行速度是很有用的。
通常,您可能会对某个大型程序进行传递,并且您有兴趣查看它进行了多少次转换。虽然您可以通过手工检查或一些特别的方法来实现这一点,但是对于大型程序来说,这确实是一个痛苦的过程,并且不是很有用。使用statistics类可以非常容易地跟踪这些信息,并且以统一的方式显示计算出的信息,并执行其余的传递。
统计的用法有很多例子,但是使用它的基础如下:
定义您的统计数据如下:
#define DEBUG_TYPE "mypassname" // This goes before any #includes.
STATISTIC(NumXForms, "The # of times I did stuff");
统计宏定义一个静态变量,其名称由第一个参数指定。传递名称取自DEBUG_TYPE宏,描述取自第二个参数。定义的变量(本例中为“NumXForms”)的作用类似于无符号整数。
每当你进行转换时,敲一下计数器:
++NumXForms; // I did stuff!
这就是你要做的。若要“选择”列印所收集的统计数字,请使用“-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
显然,有了这么多的优化,为这些东西提供一个统一的框架是非常好的。使您的传递很好地适应框架,使其更易于维护和使用。
3.8 添加调试计数器来帮助调试代码
有时,在编写新的传递或尝试跟踪bug时,能够控制传递中的某些事情是否发生是很有用的。例如,有时候最小化工具只能很容易地为您提供大型测试用例。您希望使用二分法自动地将错误缩小到发生或不发生的特定转换。这就是调试计数器的作用所在。它们提供了一个框架,使代码的某些部分只执行一定次数。
llvm/Support/DebugCounter.h (doxygen)文件提供了一个名为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
它将删除数字%2和%3。
在utils/bisect-skip-count中提供了一个实用程序,用于二进制搜索跳过和计数参数。它可用于自动最小化跳过并对调试计数器变量进行计数。
3.9 调试代码时查看图形
LLVM中的几个重要数据结构是图:例如由LLVM基块构成的CFGs、由LLVM machine基块构成的CFGs和指令选择DAGs。在许多情况下,在调试编译器的各个部分时,最好能立即可视化这些图。
LLVM提供了几个回调函数,可以在调试构建中使用。例如,如果您调用函数::viewCFG()方法,当前的LLVM工具将弹出一个包含函数CFG的窗口,其中每个基本块是图中的一个节点,每个节点包含块中的指令。类似地,还存在函数::viewCFGOnly()(不包括指令)、MachineFunction::viewCFG()和MachineFunction::viewCFGOnly()以及SelectionDAG::viewGraph()方法。例如,在GDB中,您通常可以使用诸如DAG.viewGraph()之类的东西来弹出窗口。或者,您可以将对这些函数的调用分散到您希望调试的代码中。
要使它工作,需要少量的设置。在使用X11的Unix系统上,安装graphviz工具包,并确保“dot”和“gv”位于您的路径中。如果你在Mac OS X上运行,下载并安装Mac OS X Graphviz程序,然后添加/Applications/Graphviz。app/Contents/MacOS/(或安装它的任何地方)到您的路径。在配置、构建或运行LLVM时,这些程序不需要出现,在活动调试会话期间,只需在需要时安装即可。
SelectionDAG经过扩展,可以更容易地在大型复杂图中找到感兴趣的节点。从gdb,如果你打电话给DAG。然后下一个调用dg . viewgraph()将突出显示指定颜色的节点(颜色的选择可以在color中找到)。可以通过调用DAG提供更复杂的节点属性。setGraphAttrs(节点,“attributes”)(可以在Graph attributes中找到选项)。如果希望重新启动并清除所有当前图形属性,那么可以调用DAG.clearGraphAttrs()。
注意,图形可视化特性是在版本构建之外编译的,以减小文件大小。这意味着您需要一个Debug+ assert或Release+ assert构建来使用这些特性。
4. 为任务选择正确的数据结构
LLVM /ADT/目录中有大量的数据结构,我们通常使用STL数据结构。本节描述在选择时应该考虑的权衡。
第一步是选择您自己的冒险:您想要顺序容器、类似于集合的容器还是类似于映射的容器?在选择容器时,最重要的是计划如何访问容器的算法属性。基于此,你应该使用:
- 如果需要基于另一个值高效地查找值,则使用类似于映射的容器。类映射容器还支持有效的包含查询(无论键是否在映射中)。类映射容器通常不支持有效的反向映射(值到键)。如果需要,可以使用两个映射。一些类似于映射的容器还支持按顺序高效地遍历键。类映射容器是最昂贵的一种,只有在需要这些功能之一时才使用它们。
- 如果您需要将一堆东西放入一个容器中,这个容器可以自动消除重复。一些类似集合的容器支持按排序顺序对元素进行有效的迭代。类集合容器比顺序容器更昂贵。
- 顺序容器提供了最有效的方法来添加元素,并跟踪它们添加到集合中的顺序。它们允许复制并支持有效的迭代,但不支持基于键的高效查找。
- 字符串容器是用于字符或字节数组的专用顺序容器或引用结构。
- 位容器提供了一种有效的方法来存储和执行数字id集上的set操作,同时自动消除重复。要存储的每个标识符,位容器最多需要1位。
一旦确定了容器的适当类别,您就可以通过明智地选择类别中的成员来微调内存使用、常量因素和缓存访问行为。请注意,常量因素和缓存行为可能很重要。例如,如果您有一个向量,它通常只包含几个元素(但是可以包含许多元素),那么使用SmallVector比使用vector要好得多。这样做可以避免(相对)昂贵的malloc/free调用,这大大降低了向容器添加元素的成本。
4.1 Sequential Containers (std::vector, std::list, etc)
根据您的需要,可以使用各种顺序容器。在本节中选择第一个可以做您想做的事情。
4.1.1 lvm/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[])也很简单。如果元素的数量是可变的,如果您知道在分配数组之前需要多少元素,如果数组通常很大(如果不是,请考虑一个小向量),那么它们是很有用的。堆分配数组的成本是new/delete(又名malloc/free)的成本。还请注意,如果使用构造函数分配类型的数组,则将为数组中的每个元素运行构造函数和析构函数(重新调整大小的向量只构造实际使用的元素)。
4.1.4 llvm/ADT/TinyPtrVector.h
TinyPtrVector是一个高度专门化的集合类,当一个向量有零个或一个元素时,优化它以避免分配。它有两个主要限制:1)它只能保存指针类型的值,2)它不能保存空指针。
由于这个容器高度专门化,所以很少使用它。
4.1.5 llvm/ADT/SmallVector.h
SmallVector<type, n="">是一个看起来和闻起来都像vector的简单类:它支持高效的迭代,以内存顺序排列元素(这样您就可以在元素之间执行指针算术),支持高效的push_back/pop_back操作,支持对其元素的高效随机访问,等等。</type,>
SmallVector的主要优点是它为对象本身中的一些元素(N)分配了空间。因此,如果小向量动态地小于N,则不执行malloc。如果malloc/free调用比处理元素的代码昂贵得多,那么这将是一个巨大的胜利。
这是有利于向量”通常小”(如前辈的数量/继任者的一块通常小于8)。另一方面,这使得SmallVector本身的尺寸大,所以你不想分配很多(这样做会浪费很多空间)。因此,在堆栈上使用小向量是最有用的。
SmallVector还为alloca提供了一个很好的可移植性和高效的替代品。
SmallVector相对于std::vector还有一些其他的小优势,这使得SmallVector<type, 0="">优于std::vector。</type,>
- vector是异常安全的,一些实现具有悲观的特性,当SmallVector移动元素时,它会复制这些元素。
- SmallVector理解llvm::is_trivially_copyable,并积极使用realloc。
- 许多LLVM api都将SmallVectorImpl作为out参数(参见下面的说明)。
- 在64位平台上,N = 0的SmallVector比std::vector小,因为它使用无符号(而不是void*)表示其大小和容量。
请注意
最好使用SmallVectorImpl作为参数类型。
在不关心“小尺寸”(大多数?)的api中,更喜欢使用SmallVectorImpl类,它基本上就是一个“vector
header”(和方法),后面没有分配元素。注意,SmallVector<t,
n="">继承自SmallVectorImpl,所以转换是隐式的,不需要花费任何代价。</t,>如。
// BAD: Clients cannot pass e.g. SmallVector<Foo, 4>.
hardcodedSmallSize(SmallVector<Foo, 2> &Out);
// GOOD: Clients can pass any SmallVector<Foo, N>.
allowsAnySmallSize(SmallVectorImpl &Out);
void someFunc() {
SmallVector<Foo, 8> Vec;
hardcodedSmallSize(Vec); // Error.
allowsAnySmallSize(Vec); // Works.
}
尽管它的名称中有“Impl”,但它的使用如此广泛,以至于它实际上不再是“实现的私有”了。像SmallVectorHeader这样的名称更合适。
4.1.6 <vector>
std: vector<t>很受欢迎和尊重。但是,由于上面列出的优点,SmallVector<t, 0="">通常是更好的选择。当您需要存储超过UINT32_MAX的元素或与期望vector:)的代码进行接口时,vector仍然很有用。
关于std::vector:避免这样的代码:
for ( … ) {
std::vector V;
// make use of V.
}
而是写成:
std::vector V;
for ( … ) {
// make use of V.
V.clear();
}
这样做将节省(至少)一个堆分配,并且循环的每次迭代都是空闲的。
4.1.7 <deque>
在某种意义上,deque是std::vector的广义版本。与std::vector类似,它提供了常量时间随机访问和其他类似属性,但它也提供了对列表前端的有效访问。它不能保证内存中元素的连续性。
作为这种额外灵活性的交换,std::deque的常数因子成本显著高于std::vector。如果可能的话,使用std::vector或其他更便宜的工具。
4.1.8 <list>
list是一个非常低效的类,很少有用。它为插入其中的每个元素执行堆分配,因此具有非常高的常数因子,特别是对于小数据类型。list也只支持双向迭代,不支持随机访问迭代。
作为这种高成本的交换,std::list支持对列表两端的有效访问(与std::deque类似,但与std::vector或SmallVector不同)。此外,std::list的迭代器失效特性比vector类更强:在列表中插入或删除元素不会使迭代器或指向列表中其他元素的指针失效。
4.1.9 llvm/ADT/ilist.h
ilist实现了一个“侵入式”的双链表。它是侵入性的,因为它要求元素存储并提供对列表的prev/next指针的访问。
ilist具有与std::list相同的缺点,而且还需要为元素类型实现ilist_traits,但是它提供了一些新的特性。特别是,它可以有效地存储多态对象,在插入或从列表中删除元素时通知traits类,并且ilists保证支持常量时间拼接操作。
这些属性正是我们想要的,比如指令和基本块,这就是为什么这些是用ilists实现的。
有关的兴趣类别可在下面的小节解释:
- ilist_traits
- iplist
- llvm / ADT / ilist_node.h
- Sentinels
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是ilist的定制机制。iplist(因此ilist)公开派生自这个traits类。
4.1.12 iplist
iplist是ilist的基础,因此支持稍微窄一点的接口。值得注意的是,没有来自t&r的插入器。
ilist_traits是这个类的公共基础,可以用于多种定制。
4.1.13 llvm/ADT/ilist_node.h
ilist_node以默认方式实现了ilist(和类似容器)所期望的正向和反向链接。
ilist_nodes被嵌入到节点类型T中,通常T公开派生自ilist_node。
4.1.14 Sentinels
ilists还有另一个必须考虑的特色。要成为c++生态系统中的好公民,它需要支持标准容器操作,例如开始和结束迭代器等。此外,在非空ilists的情况下,操作符—必须在end迭代器上正确工作。
这个问题唯一合理的解决方案是分配一个所谓的哨点和介入列表,后者作为最终迭代器,提供到最后一个元素的反向链接。但是,符合c++约定的操作符c++在哨兵之外是非法的,而且也不能取消对它的引用。
这些约束允许ilist自由地实现如何分配和存储标记。相应的策略由ilist_traits决定。默认情况下,每当需要一个标记时,T就会得到堆分配。
虽然默认策略在大多数情况下是足够的,但是当T不提供默认构造函数时,它可能会崩溃。此外,在许多伊利斯特的例子中,相关哨兵的内存开销被浪费了。为了缓解大量的t哨兵的情况,有时会使用一个技巧,导致幽灵般的哨兵。
幽灵哨兵是通过特殊制作的ilist_traits获得的,它将哨兵与内存中的ilist实例叠加在一起。指针算法用于获取与ilist的这个指针相对的标记符。ilist由一个额外的指针进行扩展,该指针充当标记的反向链接。这是幽灵哨兵中唯一可以合法进入的区域。
4.1.15 其他顺序容器选项
其他STL容器也是可用的,比如std::string。
还有各种STL适配器类,如std::queue、std::priority_queue、std::stack等。它们提供了对底层容器的简化访问,但不影响容器本身的成本。
4.2 String-like containers
在C和c++中有多种传递和使用字符串的方法,LLVM添加了一些可供选择的新选项。在这个列表中选择第一个选项来做你需要做的,它们是根据它们的相对成本排序的。
注意,通常不希望将字符串作为const char* ’ s传递。它们有很多问题,包括它们不能表示嵌入的nul(“0”)字符,而且没有有效的长度可用。“const char*”的一般替换是StringRef。
有关为api选择字符串容器的更多信息,请参见传递字符串。
4.2.1 llvm/ADT/StringRef.h
StringRef类是一个简单的值类,它包含一个指向字符和长度的指针,并且与ArrayRef类非常相关(但是专门用于字符数组)。因为StringRef携带一个长度,所以它可以安全地处理包含nul字符的字符串,获得长度不需要strlen调用,而且它甚至有非常方便的api来分割和分割它所表示的字符范围。
StringRef非常适合传递已知为活动的简单字符串,因为它们是C字符串文本、std::string、C数组或小向量。每一种情况都有一个到StringRef的有效隐式转换,这不会导致执行动态strlen。
StringRef有几个主要的限制,使得更强大的字符串容器更有用:
- 您不能直接将StringRef转换为’ const char* ',因为没有办法添加尾随nul(不像在各种更强的类上添加.c_str()方法)。
- StringRef不拥有或保留底层字符串字节。因此,它很容易导致悬空指针,并且在大多数情况下不适合嵌入数据结构(相反,使用std::string或类似的东西)。
- 出于同样的原因,如果方法“计算”结果字符串,则StringRef不能用作方法的返回值。相反,使用std:: string。
- StringRef不允许您更改指向字符串的字节,也不允许您从范围中插入或删除字节。对于这样的编辑操作,它与Twine类互操作。
由于其优点和局限性,函数接受StringRef和对象上的方法返回指向其拥有的某个字符串的StringRef是非常常见的。
4.2.2 llvm/ADT/Twine.h
Twine类用作api的中间数据类型,这些api希望获取一个可以通过一系列连接内联构建的字符串。Twine通过在堆栈上形成Twine数据类型的递归实例(一个简单的值对象)作为临时对象,将它们链接到一个树中,然后在使用Twine时将其线性化。Twine只能作为函数的参数使用,并且应该始终作为常量引用,例如:
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的api。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来删除重复项。如果您的使用模式有这两个不同的阶段(insert then query),并且可以很好地选择顺序容器,那么这种方法就非常有效。
这种组合提供了几个很好的特性:结果数据在内存中是连续的(对于缓存局部性很好),分配很少,易于寻址(最终向量中的迭代器只是索引或指针),并且可以通过标准的二分查找(例如std::lower_bound;如果您希望整个元素范围的比较相等,请使用std::equal_range)。
4.3.2 llvm/ADT/SmallSet.h
如果您有一个类似于集合的数据结构,通常比较小,并且其元素也比较小,那么SmallSet<type, n="">是一个不错的选择。</type,>这个集合有空间容纳N个元素(因此,如果这个集合动态地小于N,则不需要malloc流量),并通过简单的线性搜索访问它们。当集合超过N个元素时,它分配一个更昂贵的表示,以保证有效的访问(对于大多数类型,它返回到std::set,但是对于指针,它使用的是更好的SmallPtrSet。
这个类的神奇之处在于,它可以非常高效地处理小集合,但是可以优雅地处理非常大的集合,而不会降低效率。
4.3.3 llvm/ADT/SmallPtrSet.h
SmallPtrSet具有SmallSet的所有优点(并且SmallPtrSet透明地实现了一个SmallSet指针集)。如果执行了N次以上的插入,就会分配一个二次探测的哈希表,并根据需要进行增长,从而提供非常高效的访问(常数时间插入/删除/查询,常数因素较少),并且对malloc流量非常吝啬。
注意,与std::set不同,SmallPtrSet的迭代器在插入发生时无效。此外,迭代器访问的值不会按排序顺序访问。
4.3.4 llvm/ADT/StringSet.h
StringSet是一个围绕StringMap的瘦包装器,它允许有效地存储和检索惟一字符串。
在功能上类似于SmallSet, StringSet还支持迭代。(迭代器取消对StringMapEntry的引用,因此需要调用i->getKey()来访问StringSet的项。)另一方面,StringSet不支持SmallSet和SmallPtrSet支持的范围插入和复制构造。
4.3.5 llvm/ADT/DenseSet.h
DenseSet是一个简单的二次探测哈希表。它擅长于支持小值:它使用一个分配来保存当前插入到集合中的所有对。注意,DenseSet对值类型的要求与DenseMap相同。
4.3.6 llvm/ADT/SparseSet.h
SparseSet包含少量由中等大小的无符号键标识的对象。它使用大量内存,但提供的操作几乎与向量一样快。典型的键是物理寄存器、虚拟寄存器或编号的基本块。
SparseSet对于需要快速清除/查找/插入/删除以及在小集合上快速迭代的算法非常有用。它不用于构建复合数据结构。
4.3.7 llvm/ADT/SparseMultiSet.h
SparseMultiSet向SparseSet添加了多集行为,同时保留了SparseSet所需的属性。与SparseSet一样,它通常使用大量内存,但提供的操作几乎与向量一样快。典型的键是物理寄存器、虚拟寄存器或编号的基本块。
SparseMultiSet对于需要非常快速地清除/查找/插入/删除整个集合,以及迭代共享密钥的元素集的算法非常有用。它通常比使用复合数据结构(例如向量的向量向量,向量的映射)更有效。它不用于构建复合数据结构。
4.3.8 llvm/ADT/FoldingSet.h
FoldingSet是一个聚合类,它非常擅长于独特的昂贵的创建对象或多态对象。它是一个链接哈希表与入侵链接(需要从FoldingSetNode继承惟一的对象)的组合,使用SmallVector作为其ID进程的一部分。
考虑这样一种情况,您希望为复杂对象(例如,代码生成器中的节点)实现“getOrCreateFoo”方法。客户希望生成的类的描述(它知道的操作码和操作数),但我们不希望“新”节点,然后试着把它插入一组却发现它已经存在,此时我们需要删除它并返回的节点已经存在。
为了支持这种客户机风格,FoldingSet使用FoldingSetNodeID(它封装了SmallVector)执行查询,该id可用于描述我们想要查询的元素。查询要么返回与ID匹配的元素,要么返回一个不透明的ID,该ID指示插入应该在何处进行。ID的构造通常不需要堆流量。
因为FoldingSet使用入侵链接,所以它可以支持集合中的多态对象(例如,可以将SDNode实例与loadsdnode混合)。由于元素是单独分配的,指向元素的指针是稳定的:插入或删除元素不会使指向其他元素的指针失效。
4.3.9 <set>
std: set是一门合理的综合性课程,它在很多方面都不错,但在任何方面都很好。set为插入的每个元素分配内存(因此malloc非常密集),并且通常为集合中的每个元素存储三个指针(因此为每个元素增加了大量的空间开销)。它提供了保证的log(n)性能,从复杂性的角度来看,这并不是特别快(特别是当比较集合的元素非常昂贵时,比如字符串),并且在查找、插入和删除方面有非常高的常数因子。
set的优点是它的迭代器是稳定的(从集合中删除或插入元素不会影响迭代器或指向其他元素的指针),并且保证对集合的迭代是有序的。如果集合中的元素很大,那么指针和malloc流量的相对开销就不是什么大问题,但是如果集合中的元素很小,那么std::set几乎从来都不是一个好的选择。
4.3.10 llvm/ADT/SetVector.h
LLVM的SetVector是一个适配器类,它结合了您对集合类容器和顺序容器的选择,它提供了一个重要的属性,即使用惟一(忽略重复元素)和迭代支持高效插入。它通过在类集合容器和序列容器中插入元素来实现这一点,使用类集合容器进行惟一,使用序列容器进行迭代。
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非常昂贵:它的成本是维护map和vector的成本之和,它具有很高的复杂度,高的常量因子,并且产生了大量的malloc流量。这是应该避免的。
4.3.12 llvm/ADT/ImmutableSet.h
ImmutableSet是一个基于AVL树的不可变(函数)集实现。添加或删除元素是通过一个工厂对象完成的,并会创建一个新的ImmutableSet对象。如果给定的内容已经存在一个ImmutableSet,则返回现有的;将等式与FoldingSetNodeID进行比较。添加或删除操作的时间和空间复杂度与原始集合的大小成对数关系。
没有返回集合元素的方法,只能检查成员关系。
4.3.13 Other Set-Like Container Options
STL提供了其他几个选项,比如std::multiset和各种“hash_set”,比如容器(无论是来自c++ TR1还是来自SGI库)。我们从不使用hash_set和unordered_set,因为它们通常非常昂贵(每次插入都需要一个malloc),而且非常不可移植。
如果您对消除重复不感兴趣,那么multiset非常有用,但是它具有std::set的所有缺点。排序向量(不删除重复项)或其他方法几乎总是更好。
4.4 Map-Like Containers (std::map, DenseMap, etc)
当您希望将数据关联到键时,类似于映射的容器非常有用。像往常一样,有很多不同的方法可以做到这一点。?
4.4.1 A sorted ‘vector’
如果您的使用模式遵循严格的先插入后查询方法,那么您可以简单地使用与集合类容器的排序向量相同的方法。惟一的区别是,查询函数(使用std::lower_bound来获得有效的log(n)查找)应该只比较键,而不是键和值。这与集合的排序向量具有相同的优点。
4.4.2 llvm/ADT/StringMap.h
字符串通常用作映射中的键,并且很难有效地支持它们:它们是可变长度的,长时散列和比较效率低,复制成本高,等等。StringMap是一个专门用于处理这些问题的容器。它支持将任意字节范围映射到任意其他对象。
StringMap实现使用一个按四次探查的散列表,其中bucket存储指向分配给堆的条目的指针(以及其他一些东西)。映射中的条目必须进行堆分配,因为字符串的长度是可变的。字符串数据(key)和元素对象(value)与元素对象之后的字符串数据存储在同一个分配中。这个容器保证“(char*)(&Value+1)”指向值的键字符串。
由于以下几个原因,StringMap非常快:二次探测是非常高效缓存查找,在桶散列值的字符串不重新计算时查找一个元素,StringMap很少有接触无关的对象的内存时查找值(即使哈希碰撞发生),哈希表的增长不会再计算字符串的哈希值已经在表中,并在地图中每一对是存储在一个单一的分配(字符串数据存储在相同的分配价值的一对)。
StringMap还提供了接受字节范围的查询方法,因此只有在将值插入表中时,它才会复制字符串。
但是,不能保证StringMap迭代顺序是确定的,因此任何需要使用std::map的使用都应该使用std::map。
4.4.3 llvm/ADT/IndexedMap.h
IndexedMap是一个专门的容器,用于将小密集整数(或可映射到小密集整数的值)映射到其他类型。它在内部实现为一个向量,带有一个映射函数,该函数将键映射到密集整数范围。
这对于LLVM代码生成器中的虚拟寄存器之类的情况非常有用:它们有一个密集的映射,而编译时常量(第一个虚拟寄存器ID)可以抵消这个映射。
4.4.4 llvm/ADT/DenseMap.h
DenseMap是一个简单的二次探测哈希表。它擅长支持小键和值:它使用一个分配来保存当前插入到映射中的所有对。DenseMap是将指针映射到指针,或者将其他小类型映射到其他小类型的好方法。
但是,您应该注意到DenseMap的几个方面。与map不同,每当插入发生时,DenseMap中的迭代器都会失效。此外,由于DenseMap为大量键/值对分配空间(默认从64开始),如果键或值很大,则会浪费大量空间。最后,如果还不支持DenseMapInfo,则必须为所需的键实现DenseMapInfo的部分专门化。这需要告诉DenseMap关于它内部需要的两个特殊标记值(永远不能插入到映射中)。
DenseMap的find_as()方法支持使用备用键类型进行查找操作。这在构造普通键类型很昂贵,但与之相比很便宜的情况下非常有用。DenseMapInfo负责为所使用的每个备用键类型定义适当的比较和散列方法。
4.4.5 llvm/IR/ValueMap.h
ValueMap是围绕DenseMap将值*s(或子类)映射到另一类型的包装器。当一个值被删除或RAUW 'ed时,ValueMap将更新自己,以便将键的新版本映射到相同的值,就像键是WeakVH一样。通过将配置参数传递给ValueMap模板,您可以准确地配置这是如何发生的,以及这两个事件上还会发生什么。
4.4.6 llvm/ADT/IntervalMap.h
IntervalMap是用于小键和值的紧凑映射。它映射键间隔而不是单个键,并将自动合并相邻的间隔。当映射只包含几个间隔时,将它们存储在映射对象本身以避免分配。
IntervalMap迭代器非常大,因此不应该将它们作为STL迭代器传递。重量级迭代器允许较小的数据结构。
4.4.7 <map>
map具有与std::set相似的特性:它为插入到map中的每对数据使用一个分配,它提供了log(n)查找和一个非常大的常量因子,并对map中的每对数据施加3个指针的空间惩罚,等等。
map在键或值非常大时最有用,如果需要按排序顺序迭代集合,或者需要在map中使用稳定的迭代器(即,如果插入或删除另一个元素,它们不会失效)。
4.4.8 llvm/ADT/MapVector.h
MapVector<keyt,valuet>提供了DenseMap接口的子集。</keyt,valuet>主要的区别是迭代顺序保证为插入顺序,这使得它对于指针映射上的非确定性迭代是一种简单(但有些昂贵)的解决方案。
它是通过将键映射到键值对向量中的索引来实现的。这提供了快速查找和迭代,但有两个主要缺点:密钥存储两次,删除元素需要线性时间。如果需要删除元素,最好使用remove_if()批量删除它们。
4.4.9 llvm/ADT/IntEqClasses.h
IntEqClasses提供了小整数等价类的紧凑表示。最初,0…范围内的每个整数。n-1有它自己的等价类。可以通过将两个类代表传递给join(a, b)方法来联接类。当findLeader()返回相同的代表时,两个整数位于同一个类中。
一旦所有等价类都形成,映射就可以被压缩,因此每个整数0…n-1映射到范围为0的等价类数。m-1,其中m为等价类的总数。地图必须解压后才能重新编辑。
4.4.10 llvm/ADT/ImmutableMap.h
ImmutableMap是一个基于AVL树的不可变(函数)映射实现。添加或删除元素是通过一个工厂对象完成的,并会创建一个新的ImmutableMap对象。如果给定密钥集已经存在一个ImmutableMap,则返回现有的密钥集;将等式与FoldingSetNodeID进行比较。添加或删除操作的时间和空间复杂度与原始映射的大小成对数关系。
4.4.11 Other Map-Like Container Options
STL提供了其他几个选项,比如std::multimap和各种“hash_map”,比如容器(无论是来自c++ TR1还是来自SGI库)。我们从不使用hash_set和unordered_set,因为它们通常非常昂贵(每次插入都需要一个malloc),而且非常不可移植。
如果您想将键映射到多个值,则使用multiap非常有用,但是它具有std::map的所有缺点。排序向量或其他方法几乎总是更好的。
4.5 Bit storage containers (BitVector, SparseBitVector)
与其他容器不同,只有两个位存储容器,选择何时使用每个位存储容器相对比较简单。
一个额外的选项是std::vector:我们不鼓励使用它,原因有两个:1)许多通用编译器(例如通用可用的GCC版本)中的实现非常低效;无论如何,请不要使用它。
4.5.1 BitVector
位向量容器为操作提供了一组动态大小的位。它支持单独的位设置/测试,以及设置操作。set操作花费时间O(位向量的大小),但是操作一次执行一个单词,而不是一次执行一个位。与其他容器相比,这使得用于set操作的位向量非常快。当您期望集位的数目很高(即密集集)时,使用位向量。
4.5.2 SmallBitVector
SmallBitVector容器提供了与BitVector相同的接口,但是它针对只需要少量位(少于25位)的情况进行了优化。它还透明地支持较大的位计数,但效率略低于普通位向量,因此,SmallBitVector只应在很少使用较大的计数时使用。
此时,SmallBitVector不支持set操作(and, or, xor),其操作符[]不提供可分配的lvalue。
4.5.3 SparseBitVector
SparseBitVector容器非常类似于BitVector,但有一个主要的区别:只存储设置的位。这使得稀疏位向量在集合稀疏时比位向量的空间效率更高,并且使得集合操作O(集合位的数量)而不是O(宇宙的大小)。SparseBitVector的缺点是,随机位的设置和测试是O(N),对于较大的SparseBitVector,这可能比BitVector慢。在我们的实现中,按顺序设置或测试位(向前或向后)是O(1)最坏的情况。测试和设置128位内的位(取决于大小)的当前位也是O(1)。一般来说,在稀疏位向量中测试/设置位是O(到最后一个设置位的距离)。
5. 调试
6. 常见操作的有用提示
本节描述如何执行一些非常简单的LLVM代码转换。这意味着要给出常用习惯用法的示例,展示LLVM转换的实用方面。
因为这是一个“how-to”部分,所以您还应该阅读将要使用的主要类。核心LLVM类层次结构参考资料包含您应该了解的主要类的详细信息和描述。
6.1 基本检查和遍历例程
LLVM编译器基础架构有许多可以遍历的不同数据结构。以C++标准模板库为例,用于遍历这些不同数据结构的技术基本上是相同的。对于一个可枚举的值序列,XXXbegin()函数(或方法)返回一个序列开头的迭代器,在XXXend()函数返回一个迭代器,该迭代器指向序列最后一个有效元素之后的元素,这两个操作之间有一些XXXiterator数据类型是常见的。
因为迭代的模式在程序表示的许多不同方面都是通用的,所以可以在它们上使用标准模板库算法,并且更容易记住如何迭代。首先,我们展示一些需要遍历的数据结构的常见示例。以非常相似的方式遍历其他数据结构。
6.1.1 遍历一个Function中的BasicBlock
有一个你想要以某种方式转换的函数实例是很常见的;特别是,您希望操作它的基本块。为了实现这一点,您需要遍历构成该Function的所有BasicBlocks。下面是打印一个BasicBlock的名称和它包含的Instructions数的例子:
Function &Func = ...
for (BasicBlock &BB : Func)
// 如果有BasicBlock,则打印它的名称,然后打印它包含的Instructions数
errs() << "Basic block (name=" << BB.getName() << ") has "
<< BB.size() << " instructions.\n";
6.1.2 遍历一个BasicBlock中的Instruction
就像在函数中处理基本块一样,很容易遍历组成基本块的各个指令。这是一个代码片段,打印出在一个基本块的每个指令:
BasicBlock& BB = ...
for (Instruction &I : BB)
// 由于操作符<<(ostream&,…)为Instruction&重载,所以下一条指令可用
errs() << I << "\n";
然而,这并不是打印BasicBlock内容的最佳方式!由于ostream操作符实际上重载了您所关心的所有内容,所以您可以调用基本块本身上的打印例程:errs() << BB << "\n";
。
6.1.3 遍历一个Function中的Instruction
如果您发现您通常遍历函数的基本块,然后遍历基本块的指令,那么应该使用InstIterator
。您需要include llvm/IR/InstIterator.h(doxygen),然后在代码中显式实例化InstIterator。下面是一个小例子,展示了如何将函数中的所有指令转储到标准错误流:
#include "llvm/IR/InstIterator.h"
// F是指向函数实例的指针
for (inst_iterator I = inst_begin(F), E = inst_end(F); I != E; ++I)
errs() << *I << "\n";
很容易,不是吗?您还可以使用InstIterator来用工作列表的初始内容填充工作列表。例如,如果你想初始化一个工作列表来包含函数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指向的函数中的所有指令。
6.1.4 将一个 iterator 转换为一个类指针(反之亦然)
有时候,当您手头只有一个iterator
时,获取一个类实例的引用(或指针)是很有用的。从 iterator 中提取引用或指针非常简单。假设 i 是一个BasicBlock::iterator,j是一个BasicBlock::const_iterator:
Instruction& inst = *i; // 获取对指令引用的引用
Instruction* pinst = &*i; // 获取指向指令引用的指针
const Instruction& inst = *j;
但是,您将在LLVM框架中使用的 iterator 是特殊的:它们将在需要的时候自动转换为 ptr-to-instance 类型。原本是取消对 iterator 的引用,然后获取结果的地址;代替的是:您可以简单地将 iterator 分配给适当的指针类型,然后您将获得操作的dereference和address作为分配的结果(在幕后,这是重载转换机制的结果)。因此上一个例子的第二行Instruction *pinst = &*i;
,在语义上等价于:Instruction *pinst = i;
也可以将一个类指针转换为相应的 iterator ,这是一个常量时间操作(非常有效)。下面的代码片段演示了使用LLVM iterator 提供的转换构造函数。通过使用这些,您可以显式地获取某个东西的 iterator,而无需通过对某个结构进行迭代来实际获取它:
void printNextInstruction(Instruction* inst) {
BasicBlock::iterator it(inst);
++it; // 在这一行之后,it引用*inst之后的指令
if (it != inst->getParent()->end()) errs() << *it << "\n";
}
不幸的是,这些隐式转换是有代价的;它们阻止这些 iterator 遵守标准 iterator 约定,从而使它们不能与标准算法和容器一起使用。例如,它们阻止编译下面的代码,其中B是一个BasicBlock:
llvm::SmallVector<llvm::Instruction *, 16>(B->begin(), B->end());
因此,这些隐式转换可能会在某一天被删除,并且操作符*将返回指针而不是引用。
6.1.5 查找调用站点:一个稍微复杂一点的示例
假设您正在编写一个FunctionPass
,并且希望计算整个模块(即跨所有函数)中某个函数(即某个Function *)已经在作用域中的所有位置(被调用的次数)。稍后您将了解到,您可能希望使用 InstVisitor
以一种更直接的方式来实现这一点,但是这个示例将允许我们探索如果没有InstVisitor,您将如何实现这一点。在伪代码中,我们要做的是:
initialize callCounter to zero // 将callCounter初始化为零
for each Function f in the Module
for each BasicBlock b in f
for each Instruction i in b
if (i is a CallInst and calls the given function) // i是一个CallInst,它调用给定的函数
increment callCounter // 增量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 *CallInst = dyn_cast<CallInst>(&I)) {
// 我们知道我们已经遇到了一个调用指令,所以我们需要确定它是否是m_func指向的函数的调用。
if (CallInst->getCalledFunction() == targetFunc)
++callCounter;
}
}
}
}
private:
unsigned callCounter;
};
6.1.6 以相同的方式处理 calls 和 invokes
您可能已经注意到,前面的示例有些过于简化,因为它没有处理由‘invoke’指令生成的调用站点。在这种情况下,以及在其他情况下,您可能会发现您希望以同样的方式处理 CallInsts 和 InvokeInsts,即使它们最特定的公共基类是 Instruction,其中也包含许多不那么密切相关的东西。对于这些情况,LLVM提供了一个方便的wrapper类 CallSite (doxygen),它本质上是一个围绕 Instruction 指针的wrapper,它有一些方法提供 CallInsts 和 InvokeInsts 共有的功能。
该类具有“值语义”:它应该通过值传递,而不是通过引用传递,并且不应该使用 new 或 delete 操作符动态分配或释放该类。它具有高效的可复制性、可分配性和可构造性,其成本相当于一个空指针的成本。如果你看它的定义,它只有一个指针成员。
6.1.7 遍历 def-use 和 use-def 链
通常,我们可能有一个Value
类的实例(doxygen),我们希望确定哪些Users
使用这个值。具有特定Value
的所有Users
的列表称为def-use
链。例如,我们有一个 Function* F 指向一个特定的函数 foo。找到所有使用 foo 的指令就像遍历 F 的def-use
链一样简单:
Function *F = ...;
for (User *U : F->users()) {
if (Instruction *Inst = dyn_cast<Instruction>(U)) {
errs() << "F is used in instruction:\n";
errs() << *Inst << "\n";
}
或者,通常有一个User
类的实例(doxygen),并且需要知道它使用什么Values
。一个User
使用的所有Values
的列表称为use-def
链。类Instruction
的实例是常见的User
,所以我们可能需要遍历特定Instruction使用的所有values(即特定Instruction的操作数):
Instruction *pi = ...;
for (Use &U : pi->operands()) {
Value *v = U.get();
// ...
}
将对象声明为 const 是实现无变化算法(如分析等)的一个重要工具。为此,上面的iterators有两种固定的风格:Value::const_use_iterator
和Value::const_op_iterator
。当分别调用 const Value*s
或 const User*s
上的use/op_begin()
时,它们会自动出现。在取消引用后,它们返回const Use*s
。否则,上述模式将保持不变。
6.1.8 遍历块的前置和后继
使用“llvm/IR/CFG.h”中定义的例程,遍历块的前置和后继是非常容易的。只需使用这样的代码来遍历所有BB的前置:
#include "llvm/IR/CFG.h"
BasicBlock *BB = ...;
for (BasicBlock *Pred : predecessors(BB)) {
// ...
}
类似地,要遍历后继,可以使用successors
。
6.2 做些简单的改变
LLVM基础架构中有一些基本的转换操作值得了解。在执行转换时,操作基本块的内容是相当常见的。本节描述了一些常用的方法,并给出了示例代码。
6.2.1 创建和插入新 Instructions
- 实例化 Instructions
创建Instructions非常简单:只需调用该类指令的构造函数来实例化并提供必要的参数。例如,AllocaInst
只需要提供一个(const-ptr-to) Type。因此:
这将在运行时创建一个 AllocaInst 实例,该实例表示当前堆栈帧中一个整数的分配。每个 Instruction 子类都可能有不同的默认参数,这些参数会改变这个指令的语义,所以请参考 Instruction子类的doxygen文档,以获得您感兴趣的要实例化的子类的内容。auto *ai = new AllocaInst(Type::Int32Ty);
- 命名值
如果可以的话,命名指令的值是非常有用的,因为这有助于调试您的转换。如果您最终查看生成的LLVM机器码,那么您肯定希望有与指令结果关联的逻辑名称!通过为Instruction
构造函数的Name
(缺省)参数提供一个值,您可以将逻辑名称与运行时指令执行的结果关联起来。例如,假设我正在编写一个转换,它动态地为堆栈上的一个整数分配空间,这个整数将被其他代码用作某种索引。为此,我将AllocaInst
放在某个 Function 的第一个 BasicBlock 的第一个 point 上,并打算在同一个 Function 中使用它。我可能会做:
其中auto *pa = new AllocaInst(Type::Int32Ty, 0, "indexLoc");
indexLoc
现在是指令执行值的逻辑名称,它是指向运行时堆栈上整数的指针。 - 插入 Instructions
从本质上讲,有三种方法可以将一条 Instruction 插入到构成一个 BasicBlock 的现有指令序列中:- 插入到显式指令列表中
给定一个BasicBlock* pb
,该 BasicBlock 中的一个Instruction* pi
,以及我们希望在 *pi 之前插入的一条新创建的instruction
,我们执行以下操作:
附加到一个 BasicBlock 末尾是如此常见,以至于Instruction类和Instruction派生类提供构造函数,构造函数接受要附加到 BasicBlock 的指针。例如代码如下:BasicBlock *pb = ...; Instruction *pi = ...; auto *newInst = new Instruction(...); pb->getInstList().insert(pi, newInst); // Inserts newInst before pi in pb
就变成:BasicBlock *pb = ...; auto *newInst = new Instruction(...); pb->getInstList().push_back(newInst); // Appends newInst to pb
这是非常简洁的,特别的是如果您正在创建长指令流。BasicBlock *pb = ...; auto *newInst = new Instruction(..., pb);
- 插入到隐式指令列表中
已经在 BasicBlocks 中的 Instruction 实例隐式地与现有的指令列表关联:包含基本块的指令列表。因此,我们可以完成与上述代码相同的事情,而不需要给一个基本块:
实际上,这一系列步骤发生得非常频繁,以至于 Instruction 类和 Instruction 派生类提供构造函数,构造函数(作为默认参数)接受一个指向一条 Instruction 的指针,新创建的 Instruction 应该位于这个指令之前。也就是说,Instruction 构造函数能够将新创建的实例插入到所提供的指令的 BasicBlock 中,就在该指令之前。使用带有一个 insertBefore (默认)参数的 Instruction 构造函数,上面的代码变成:Instruction *pi = ...; auto *newInst = new Instruction(...); pi->getParent()->getInstList().insert(pi, newInst);
这是非常简洁的,特别是当您创建了很多指令并将它们添加到 BasicBlocks 中时。Instruction* pi = ...; auto *newInst = new Instruction(..., pi);
- 使用 IRBuilder 的一个实例进行插入
使用前面的方法插入几条 Instructions 可能非常费力。IRBuilder
是一个方便的类,可以用来在一个 BasicBlock 的末尾或某个特定 Instruction 之前添加多个指令。它还支持常量折叠和重命名命名寄存器(参见IRBuilder
的模板参数)。
下面的示例演示了IRBuilder
的一个非常简单的用法,其中在指令 pi 之前插入了三条指令。前两条指令是 Call 指令,第三条指令乘以这两条调用的返回值。
下面的示例与上面的示例类似,只是创建的 IRBuilder 在 BasicBlock pb 的末尾插入指令。Instruction *pi = ...; IRBuilder<> Builder(pi); CallInst* callOne = Builder.CreateCall(...); CallInst* callTwo = Builder.CreateCall(...); Value* result = Builder.CreateMul(callOne, callTwo);
有关 IRBuilder 的实际使用,请参见 Kaleidoscope:对LLVM IR的代码生成。BasicBlock *pb = ...; IRBuilder<> Builder(pb); CallInst* callOne = Builder.CreateCall(...); CallInst* callTwo = Builder.CreateCall(...); Value* result = Builder.CreateMul(callOne, callTwo);
- 插入到显式指令列表中
6.2.2 删除 Instructions
从构成一个 BasicBlock 的现有指令序列中删除一条指令非常简单:只需调用该指令的eraseFromParent()
方法。例如:
Instruction *I = .. ;
I->eraseFromParent();
这将从其包含的基本块中断开指令的链接并删除它。如果只是想从包含基本块的指令中断开链接,而不是删除它,可以使用removeFromParent()
方法。
6.2.3 用另一个 Value 替换一条 Instruction
6.2.3.1 取代个别的 Instructions
包含 “llvm/Transforms/Utils/BasicBlockUtils.h” 允许使用两个非常有用的替换函数:ReplaceInstWithValue
和ReplaceInstWithInst
。
6.2.3.2 删除 Instructions
- ReplaceInstWithValue
这个函数用一个 Value 替换给定 Instruction 的所有用法,然后删除原始指令。下面的示例演示了如何用一个指向整数的空指针替换为单个整数分配内存的特定AllocaInst的结果。AllocaInst* instToReplace = ...; BasicBlock::iterator ii(instToReplace); ReplaceInstWithValue(instToReplace->getParent()->getInstList(), ii, Constant::getNullValue(PointerType::getUnqual(Type::Int32Ty)));
- ReplaceInstWithInst
该函数用另一条指令替换一条特定的指令,将新指令插入到旧指令所在的基本块中,并用新指令替换旧指令的任何用法。下面的示例演示了如何用另一个 AllocaInst 替换一个 AllocaInst:AllocaInst* instToReplace = ...; BasicBlock::iterator ii(instToReplace); ReplaceInstWithInst(instToReplace->getParent()->getInstList(), ii, new AllocaInst(Type::Int32Ty, 0, "ptrToReplacedInt"));
6.2.3.3 替换 Users 和 Values 的多个使用
您可以使用Value::replaceAllUsesWith
和User::replaceUsesOfWith
来一次更改多个使用。有关Value类和User类的更多信息,请参见doxygen文档。
6.2.4 删除 GlobalVariables
从一个 Module 中删除一个全局变量与删除一条指令一样简单。首先,必须有一个指向要删除的全局变量的指针。您可以使用这个指针将它从其parent(即Module)中删除。例如:
GlobalVariable *GV = .. ;
GV->eraseFromParent();
7. 线程和LLVM
本节描述LLVM APIs与多线程的交互,包括客户端应用程序的交互和JIT中的交互,以及托管应用程序中的交互。
注意,LLVM对多线程的支持仍然相对较年轻。在2.5版之前,支持线程托管应用程序的执行,但不支持线程客户机访问APIs。虽然现在支持这个用例,但是客户端必须遵守下面指定的指导原则,以确保在多线程模式下正确操作。
注意,在类unix平台上,为了支持线程操作,LLVM需要GCC的原子内部特性。如果需要在没有合适的现代系统编译器的平台上使用支持多线程的LLVM,可以考虑在单线程模式下编译LLVM和LLVM-GCC,并使用生成的编译器构建支持多线程的LLVM副本。
7.1 使用llvm_shutdown()结束执行
使用LLVM api之后,应该调用llvm_shutdown()来释放用于内部结构的内存。
7.2 使用ManagedStatic延迟初始化
ManagedStatic是LLVM中的一个实用程序类,用于实现静态资源的静态初始化,比如全局类型表。在单线程环境中,它实现了一个简单的延迟初始化方案。然而,在编译支持多线程的LLVM时,它使用双重检查锁定来实现线程安全的延迟初始化。
7.3 使用LLVMContext实现隔离
LLVMContext是LLVM API中的一个不透明类,客户端可以使用它在同一个地址空间内并发地操作多个独立的LLVM实例。例如,在假设的编译服务器中,单个翻译单元的编译在概念上独立于所有其他单元,并且希望能够在独立的服务器线程上同时编译传入的翻译单元。幸运的是,LLVMContext只支持这种场景!
从概念上讲,LLVMContext提供了隔离。LLVM内存IR中的每个LLVM实体(模块、值、类型、常量等)都属于一个LLVMContext。不同上下文中的实体不能相互交互:不同上下文中的模块不能链接在一起,不同上下文中的函数不能添加到模块中,等等。这意味着在多个线程上同时编译是安全的,只要没有两个线程对同一上下文中的实体进行操作。
实际上,除了类型创建/查找API之外,API中很少有地方需要LLVMContext的显式规范。因为每种类型都带有对其所属上下文的引用,所以大多数其他实体可以通过查看自己的类型来确定它们属于哪个上下文。如果您正在向LLVM IR添加新实体,请尝试维护此接口设计。
7.4 线程和JIT
LLVM的“eager”JIT编译器在线程程序中使用是安全的。多个线程可以并发地调用ExecutionEngine::getPointerToFunction()或ExecutionEngine::runFunction(),多个线程可以并发地运行JIT输出的代码。用户仍然必须确保只有一个线程访问给定LLVMContext中的IR,而另一个线程可能正在修改它。一种方法是在访问JIT外部的IR时始终保持JIT锁(JIT通过添加CallbackVHs来修改IR)。另一种方法是只从LLVMContext的线程调用getPointerToFunction()。
当JIT被配置为延迟编译(使用ExecutionEngine:: disablelazycompile (false))时,当前在延迟jated函数之后更新调用站点时存在竞争条件。如果您确保每次只有一个线程可以调用任何特定的延迟存根,并且JIT锁保护任何IR访问,那么仍然可以在线程程序中使用延迟JIT,但是我们建议只在线程程序中使用即时JIT。
8. 高级的主题
本节描述了大多数客户端不需要知道的一些高级或晦涩的API。这些API往往管理LLVM系统的内部工作,只需要在不寻常的情况下访问。
8.1 ValueSymbolTable类
ValueSymbolTable (doxygen)类提供了一个符号表,Function和Module类使用这个符号表来命名value定义。符号表可以为任何Value提供一个名称。
注意,SymbolTable类不应该被大多数客户机直接访问。它只应该在需要遍历符号表名称本身时使用,这是非常特殊的用途。注意,并非所有LLVM Values都有名称,没有名称的Value
(即它们有一个空名称)在符号表中不存在。
符号表支持使用begin/end/iterator
对符号表中的values
进行迭代,并支持查询符号表中是否有特定的名称(通过lookup
)。ValueSymbolTable类不公开任何public改变对象属性的方法 ,而是简单地对一个value调用setName,这将会自动将其插入到适当的符号表中。
8.2 User
和owned Use
类的内存布局
User
(doxygen)类为表示User对其他Value instances的所有权提供了基础。Use
(doxygen) helper类用于簿记和促进*O(1)*的添加和删除。
8.2.1 User
和Use
对象之间的交互和关系
User
的一个子类可以选择合并它的Use
对象,也可以通过指针在行外引用它们。混合变量(有些Use
s内联,有些挂起)是不切实际的,打破属于同一User
的Use
对象组成连续数组的不变量。
我们在User(子)类有2种不同的布局:
- Layout a)
Use对象位于User对象的内部(resp. 在固定偏移量),它们的数量是固定的 - Layout b)
Use对象由指向User对象的一个数组的一个指针引用,它们的数量可能是可变的。
从v2.4开始,每个布局仍然拥有指向使用数组开始的直接指针。虽然Layout a)不是强制性的,但是为了简单起见,我们坚持使用这种冗余。User对象还存储它拥有的使用对象的数量。(从理论上讲,这个信息也可以通过下面的方案计算出来。)
特殊形式的分配操作符(操作符new)强制执行以下内存布局: - Layout a)通过Use[]数组在User对象前面加上前缀来建模。
...---.---.---.---.-------... | P | P | P | P | User '''---'---'---'---'-------'''
- Layout b)通过指向Use[]数组来建模。
.-------... | User '-------''' | v .---.---.---.---... | P | P | P | P | '---'---'---'---'''
(在上面的图中,“P
”代表Use**
,它存储在成员Use::Prev
中的每个Use
对象中)
8.2.2 waymarking算法
由于Use对象被剥夺了指向其用户对象的直接(反向)指针,因此必须有一种快速而精确的方法来恢复它。这可以通过以下方案来实现:
一个位编码在2 LSBits(最不重要的位)的使用::Prev允许找到用户对象的开始:
- 00 —— 二进制数字0
- 01 —— 二进制数字1
- 10 —— 停止计算(s)
- 11 —— full stop句号(S)
给定一个Use*,我们所要做的就是一直走到我们到达一个停站,或者我们有一个用户紧跟在后面,或者我们必须走到下一个停站,拿起数字并计算偏移量:
.---.---.---.---.---.---.---.---.---.---.---.---.---.---.---.---.----------------
| 1 | s | 1 | 0 | 1 | 0 | s | 1 | 1 | 0 | s | 1 | 1 | s | 1 | S | User (or User*)
'---'---'---'---'---'---'---'---'---'---'---'---'---'---'---'---'----------------
|+15 |+10 |+6 |+3 |+1
| | | | | __>
| | | | __________>
| | | ______________________>
| | ______________________________________>
| __________________________________________________________>
只需要在停止之间存储大量的位,所以最坏的情况是,当有1000个使用对象与一个用户关联时,需要进行20次内存访问。
8.2.3 参考实现
下面的文字Haskell片段演示了这个概念:
> import Test.QuickCheck
>
> digits :: Int -> [Char] -> [Char]
> digits 0 acc = '0' : acc
> digits 1 acc = '1' : acc
> digits n acc = digits (n `div` 2) $ digits (n `mod` 2) acc
>
> dist :: Int -> [Char] -> [Char]
> dist 0 [] = ['S']
> dist 0 acc = acc
> dist 1 acc = let r = dist 0 acc in 's' : digits (length r) r
> dist n acc = dist (n - 1) $ dist 1 acc
>
> takeLast n ss = reverse $ take n $ reverse ss
>
> test = takeLast 40 $ dist 20 []
>
打印<test>给出:“1s100000s11010s10100s1111s1010s110s11s1S”
反向算法仅通过检查某个前缀来计算字符串的长度:
> pref :: [Char] -> Int
> pref "S" = 1
> pref ('s':'1':rest) = decode 2 1 rest
> pref (_:rest) = 1 + pref rest
>
> decode walk acc ('0':rest) = decode (walk + 1) (acc * 2) rest
> decode walk acc ('1':rest) = decode (walk + 1) (acc * 2 + 1) rest
> decode walk acc _ = walk + acc
>
现在,正如预期的那样,打印<pref test>得到40。
我们可以快速检查这个与以下属性:
> testcase = dist 2000 []
> testcaseLength = length testcase
>
> identityProp n = n > 0 && n <= testcaseLength ==> length arr == pref arr
> where arr = takeLast n testcase
>
正如所料,<quickCheck identityProp>给出:
*Main> quickCheck identityProp
OK, passed 100 tests.
让我们更详尽一点:
>
> deepCheck p = check (defaultConfig { configMaxTest = 500 }) p
>
下面是的结果:
*Main> deepCheck identityProp
OK, passed 500 tests.
8.2.4 标签注意事项
为了维护每个Use的2个LSBits在设置好之后不会改变的不变,setters of Use::Prev必须在每次修改时重新标记新的Use。因此getter程序必须剥离标记位。
对于布局b),我们没有找到用户,而是找到一个指针(User* with LSBit set)。跟着这个指针,我们就可以看到用户了。一个可移植的技巧确保用户的第一个字节(如果解释为指针)永远不会设置LSBit(可移植性依赖于所有已知编译器都将vptr放在实例的第一个单词中)。
8.3 设计类型层次结构和多态接口
有两种不同的设计模式可能导致在c++程序的类型层次结构中使用虚分派方法。第一个是真正的类型层次结构,层次结构中的不同类型为功能和语义的特定子集建模,这些类型严格地彼此嵌套。这方面的好例子可以在值或类型类型层次结构中看到。
其次是希望跨多态接口实现集合动态分派。通过定义一个抽象接口基类,可以使用虚拟分派和继承对后一种用例建模,所有实现都从这个抽象接口基类派生并覆盖它。然而,这种实现策略强制存在一个实际上没有意义的“is-a”关系。通常没有一些有用的泛型的嵌套层次结构,代码可以与之交互并上下移动。相反,有一个单一的接口,它跨一系列实现进行分派。
第二个用例的首选实现策略是泛型编程(有时称为“编译时duck类型化”或“静态多态性”)。例如,可以跨任何符合接口或概念的特定实现实例化某个类型参数T上的模板。这里的一个很好的例子是,任何类型的高度泛型属性都可以对有向图中的节点建模。LLVM主要通过模板和泛型编程对这些进行建模。这样的模板包括LoopInfoBase和DominatorTreeBase。当这种类型的多态性真正需要动态分派时,可以使用一种称为基于概念的多态性的技术对其进行概括。此模式使用一种非常有限的虚拟分派形式来模拟模板的接口和行为,以便在其实现内部擦除类型。你可以在PassManager.h系统中找到这种技术的例子,Sean Parent在他的几篇演讲和论文中对此有更详细的介绍:
- 继承是邪恶的基本类别——2013年描述这种技术的本地谈话,可能是最好的起点。
- 值语义和基于概念的多态性- c++现在!2012年的talk更详细地描述了这种技术。
- Sean Parent的论文和演示文稿——一个Github项目,包含了幻灯片、视频和代码的链接。
在决定是创建类型层次结构(使用标记或虚拟分派)还是使用模板或基于概念的多态性时,请考虑是否对抽象基类进行某种细化,该类是接口边界上语义上有意义的类型。如果任何比根抽象接口更精细的东西作为语义模型的部分扩展都没有任何意义,那么您的用例可能更适合多态性,您应该避免使用虚拟分派。然而,可能有一些紧急情况需要使用其中一种技术。
如果您确实需要引入类型层次结构,我们更愿意使用带有手动标记分派和/或RTTI的显式封闭类型层次结构,而不是在c++代码中更常见的开放继承模型和虚拟分派。这是因为LLVM很少鼓励库使用者扩展其核心类型,并且利用其层次结构的封闭和标记分派特性来生成更有效的代码。我们还发现,大量使用类型层次结构更适合基于标记的模式匹配,而不是跨公共接口进行动态分派。在LLVM中,我们构建了自定义帮助程序来促进这种设计。请参阅本文档关于isa和dyn_cast的部分,以及我们的详细文档,该文档描述了如何实现此模式,以便与LLVM helper一起使用。
8.4 ABI打破检查
检查并断言更改LLVM c++ ABI是基于预处理器符号llvm_enable_abi_breaking_check来断言的——使用llvm_enable_abi_breaking_check构建的LLVM库与没有定义的LLVM库是不兼容的。默认情况下,打开断言也会打开llvm_enable_abi_breaking_check,因此默认的+断言构建与默认的-断言构建不兼容。希望在+断言和-断言构建之间具有ABI兼容性的客户端应该使用CMake或autoconf构建系统来设置llvm_enable_abi_breaking_check,独立于llvm_enable_assertion。
9. 核心LLVM类层次结构参考资料
#include “llvm/IR/Type.h”
header source: Type.h
doxygen info: Type Clases
核心LLVM类是表示被检查或转换的程序的主要方法。核心LLVM类在 include/llvm/IR
目录的头文件中定义,并在lib/IR
目录中实现。值得注意的是,由于历史原因,这个库被称为libLLVMCore.so
,并不是你所想的libLLVMIR.so
。
9.1 Type
类和派生类型
Type
是所有type类的一个超类。每个Value
都有一个Type
。Type不能直接实例化,只能通过其子类实例化。某些基本类型(VoidType、LabelType、FloatType和DoubleType)有隐藏的子类。之所以隐藏它们,是因为除了Type类提供的功能之外,它们没有提供任何有用的功能,除了将它们与Type的其他子类区分开来之外。
所有其他类型都是DerivedType
的子类。Types
可以被命名,但这不是必需的。一个给定形状在任何时候都只存在一个实例。这允许使用Type实例的地址相等来执行type相等。也就是说,给定两个Type*值,如果指针相同,则types相同。
9.1.1 重要的Public方法
bool isIntegerTy() const
:对任何整数类型返回true。bool isFloatingPointTy()
:如果这是五种浮点类型之一,则返回true。bool issize()
:如果类型的大小已知,则返回true。没有大小的是抽象类型、标签和void。
9.1.2 重要的派生类型
- IntegerType
DerivedType的子类,表示任意位宽的整数类型。在IntegerType::MIN_INT_BITS (1)
和IntegerType::MAX_INT_BITS (~8 million)
之间的任何位宽都可以被表示。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
类似,但区别在于它是一个first class type,而ArrayType不是。向量类型用于向量操作,通常是一个整数或浮点类型的小向量。 - StructType
DerivedTypes
的子类,用于struct类型。 - FunctionType
DerivedTypes
的子类,用于function类型。bool isVarArg() cons
:如果它是一个vararg函数,则返回true。const Type * getReturnType() const
:返回函数的返回类型。const Type * getParamType (unsigned i)
:返回第i个参数的类型。const unsigned getNumParams() const
:返回形式参数的数量。
9.2 Module类
#include “llvm/IR/Module.h”
header source: Module.h
doxygen info: Module Class
Module
类表示LLVM程序中的顶层结构。一个LLVM Module实际上要么是原始程序的一个翻译单元,要么是链接器合并的几个翻译单元的一个组合。Module
类跟踪一个Functions列表、一个GlobalVariables列表和一个SymbolTable。此外,它还包含一些有用的成员函数,这些函数试图简化常见操作。
9.2.1 Module类的重要Public成员
Module::Module(std::string name = "")
构造一个Module很容易。您可以选择为它提供一个名称(可能基于翻译单元的名称)。Module::iterator
—— 函数列表iterator的类型定义
Module::const_iterator
—— const_iterator的类型定义。
begin(), end(), size(), empty()
这些转发方法使访问Module对象的Function列表的内容变得很容易。Module::FunctionListType &getFunctionList()
返回Function列表。当您需要更新列表或执行没有转发方法的复杂操作时,这是必需的。Module::global_iterator
—— 全局变量列表iterator的类型定义
Module::const_global_iterator
—— const_iterator的类型定义。
global_begin(), global_end(), global_size(), global_empty()
这些转发方法使访问Module对象的GlobalVariable列表的内容变得很容易。Module::GlobalListType &getGlobalList()
返回GlobalVariables列表。当您需要更新列表或执行没有转发方法的复杂操作时,这是必需的。SymbolTable *getSymbolTable()
返回对这个Module的SymbolTable的一个引用。Function *getFunction(StringRef Name) const
在Module SymbolTable中查找指定的函数。如果不存在,返回null。FunctionCallee getOrInsertFunction(const std::string &Name, const FunctionType *T)
在Module SymbolTable中查找指定的函数。如果它不存在,则为函数添加一个外部声明并返回它。注意,已经存在的函数签名可能与请求的签名不匹配。因此,为了支持将结果直接传递给EmitCall的常见用法,返回类型是{FunctionType *T, Constant *FunctionPtr}
的一个结构体,而不是具有潜在的意外签名的简单Function*
。std::string getTypeName(const Type *Ty)
如果指定Type的SymbolTable中至少有一个条目,则返回它。否则返回空字符串。bool addTypeName(const std::string &Name, const Type *Ty)
在将Name映射到Ty的SymbolTable中插入一个条目。如果已经有该名称的条目,则返回true,并且不修改SymbolTable。
9.3 Value类
#include “llvm/IR/Value.h”
header source: Value.h
doxygen info: Value Class
Value
类是LLVM源库中最重要的类。它表示一个类型化值,可以(除其他外)用作一条指令的操作数。有许多不同类型的Values,比如常量、参数。甚至指令和函数也是Values。
一个特定的Value可以在程序的LLVM表示中多次使用。例如,一个函数的一个传入参数(用Argument
类的一个实例表示)被引用该参数的函数中的每条指令“使用”。为了跟踪这种关系,Value类保存了使用它的所有Users的一个列表(User类是LLVM图中所有可以引用Values的节点的基类)。这个use列表是LLVM在程序中表示def-use
信息的方式,并且可以通过use_*
方法访问,如下所示。
因为LLVM是一个类型化表示,所以每个LLVM Value都是类型化的,并且这种Type可以通过getType()
方法获得。此外,所有LLVM values都可以被命名。Value的“name”是可在LLVM代码中打印的一个符号字符串:
%foo = add i32 1, 2
这个指令的名称是“foo”。注意,任何值的名称都可能丢失(一个空字符串),所以名称应该只用于调试(使源代码更容易阅读,调试打印输出),不应该用于跟踪值或在它们之间映射。为此,使用指向这个Value本身的一个std::map
代替。
LLVM的一个重要方面是,SSA变量和生成它的操作之间没有区别。因此,任何对指令生成的值的引用(例如,作为传入参数可用的值)都表示为指向表示该值的类实例的直接指针。虽然这可能需要一些时间来适应,但它简化了表示,使操作更容易。
9.3.1 Value类的重要Public成员
Value::use_iterator
—— use-list上的iterator的类型定义
Value::const_use_iterator
—— use-list上的const_iterator的类型定义
unsigned use_size()
—— 返回这个value的users数量。
bool use_empty()
—— 如果没有users,返回true。
use_iterator use_begin()
—— 获取指向use-list的开始的一个迭代器。
use_iterator use_end()
—— 获取指向use-list的结尾的一个迭代器。
User *use_back()
—— 返回列表中的最后一个元素。
这些方法是访问LLVM中的def-use
信息的接口。与LLVM中的所有其他iterators一样,命名约定遵循STL定义的约定。Type *getType() const
这个方法返回Value的Type。bool hasName() const
std::string getName() const
void setName(const std::string &Name)
这类方法用于访问和为Value分配名称,请注意上面的预防措施。void replaceAllUsesWith(Value *V)
此方法遍历一个Value
的use列表
,它更改当前value的所有Users
以引用“V”。例如,如果您检测到一条指令总是产生一个常量值(例如通过常量折叠),您可以像这样用常量替换该指令的所有用法:
Inst->replaceAllUsesWith(ConstVal);
9.4 User类
#include “llvm/IR/User.h”
header source: User.h
doxygen info: User Class
Superclass: Value
User
类是所有可能引用Values
的LLVM节点的公共基类。它公开了一个“操作数”
列表,这些“操作数”是User
引用的所有Values
。User类本身是Value的子类。
User的操作数直接指向它引用的LLVM Value。因为LLVM使用静态单赋值(SSA)表单,所以只能引用一个定义,从而允许这种直接连接。这个连接在LLVM中提供use-def信息。
9.4.1 User类的重要Public成员
User类以两种方式公开操作数列表:通过一个索引访问接口和一个基于iterator的接口。
Value *getOperand(unsigned i)
unsigned getNumOperands()
这两种方法以一个方便直接访问的形式公开User的操作数。User::op_iterator
—— 操作数列表上的iterator的类型定义
op_iterator op_begin()
—— 获取指向操作数列表的开始的一个迭代器。
op_iterator op_end()
—— 获取指向操作数列表的末尾的一个迭代器。
这些方法一起组成了一个User操作数的基于iterator的接口。
9.5 Instruction类
#include “llvm/IR/Instruction.h”
header source: Instruction.h
doxygen info: Instruction Class
Superclasses: User, Value
Instruction
类是所有LLVM指令的公共基类。它只提供了几个方法,但是是一个非常常用的类。Instruction
类本身跟踪的主要数据是操作码(指令类型)和嵌入Instruction的父BasicBlock。为了表示一个特定类型的指令,使用了众多Instruction子类中的一个。
因为Instruction
类是User
类的子类,所以可以像访问其他Users一样访问它的操作数(使用getOperand()/getNumOperands()
和op_begin()/op_end()
方法)。Instruction类的一个重要文件是llvm/Instruction.def
文件。这个文件包含一些关于LLVM中各种不同类型指令的元数据
。它描述了用作操作码
的enum值(例如,Instruction::Add
和Instruction::ICmp
),以及实现该指令的具体Instruction子类(例如,BinaryOperator和CmpInst)。不幸的是,这个文件中宏的使用混淆了doxygen,所以这些enum值没有正确地显示在doxygen输出中。
9.5.1 Instruction类的重要子类
- BinaryOperator
这个子类表示所有两个操作数指令,它们的操作数必须是相同的类型,比较指令除外。 - CastInst
这个子类是12个casting指令的父类。它提供了对cast指令的通用操作。 - CmpInst
这个子类表示两个比较指令,ICmpInst(整数操作数)和FCmpInst(浮点操作数)。
9.5.2 Instruction类的重要Public成员
- BasicBlock *getParent()
返回嵌入该 Instruction 的BasicBlock。 - bool mayWriteToMemory()
如果指令(即call、free、invoke或store)写入内存,则返回true。 - unsigned getOpcode()
返回该 Instruction 的操作码。 - Instruction *clone() const
返回指定指令的另一个实例,该实例在所有方面与原始指令相同,只是该指令没有parent(即没有嵌入到BasicBlock中),而且没有名称。
9.6 Constant类和子类
Constant
表示不同类型常量的基类。它由ConstantInt
、ConstantArray
等构成子类,用于表示各种类型的Constants。GlobalValue也是一个子类,它表示全局变量或函数的地址。
9.6.1 Constant类的重要子类
ConstantInt
:Constant
的子类表示任意宽度的一个整数常量。const APInt& getValue() const
:返回这个常量的底层值,一个APInt值。int64_t getSExtValue() const
:通过符号扩展将底层APInt值转换为int64_t。如果APInt的值(而不是位宽)太大,无法容纳int64_t,则会生成一个断言。由于这个原因,不鼓励使用这种方法。uint64_t getZExtValue() const
:通过zero扩展将底层APInt值转换为uint64_t。如果APInt的值(而不是位宽)太大,无法放入uint64_t中,则会生成一个断言。由于这个原因,不鼓励使用这种方法。static ConstantInt* get(const APInt& Val)
:返回代表Val提供的值的ConstantInt对象。该类型被暗示为与Val的位宽相对应的整数类型。static ConstantInt* get(const Type *Ty, uint64_t Val)
:返回代表Val为整数类型Ty提供的值的ConstantInt对象。
ConstantFP
:这个类表示一个浮点常量。double getValue() const
:返回这个常量的基础值。
ConstantArray
:这表示一个常量数组。const std::vector<Use> &getValues() const
:返回组成这个数组的一个组件常量向量。
ConstantStruct
:这表示一个常量Struct。const std::vector<Use> &getValues() const
:返回组成这个struct的一个组件常量向量。
GlobalValue
:它表示一个全局变量或函数。在这两种情况下,值都是一个常量固定地址(链接之后)。
9.7 GlobalValue类
#include “llvm/IR/GlobalValue.h”
header source: GlobalValue.h
doxygen info: GlobalValue Class
Superclasses: Constant, User, Value
GlobalValue(GlobalVariables 或 Functions)是所有函数体中唯一可见的LLVM values。因为它们在全局范围内是可见的,所以它们还受制于与其他在不同翻译单元中定义的全局变量的链接。为了控制链接过程,GlobalValues知道它们的 linkage 规则。具体地说,GlobalValues知道它们是否具有internal或external linkage,这是由LinkageTypes枚举定义的。
如果一个GlobalValue有internal linkage(相当于C语言中的static链接),那么它对于当前翻译单元之外的代码是不可见的,并且不参与链接。如果它有external linkage,那么它对外部代码是可见的,并且确实参与了链接。除了linkage信息,GlobalValues还跟踪它们当前属于哪个Module。
因为GlobalValues是内存对象,所以它们总是由它们的地址来引用。因此,一个全局的Type始终是指向其内容的一个指针。在使用GetElementPtrInst指令时,一定要记住这一点,因为必须首先取消对该指针的引用。例如,如果您有一个GlobalVariable (GlobalValue的子类),它是一个24 int的数组,类型为[24xi32],那么GlobalVariable是指向该数组的指针。虽然这个数组的第一个元素的地址和GlobalVariable的值是相同的,但是它们有不同的类型。全局变量的类型是[24xi32]。第一个元素的类型是i32。因此,访问一个global value需要您首先用GetElementPtrInst取消对指针的引用,然后才能访问它的元素。这在LLVM语言参考手册中有解释。
9.7.1 GlobalValue类的重要Public成员
- bool hasInternalLinkage() const
bool hasExternalLinkage() const
void setInternalLinkage(bool HasInternalLinkage)
这些方法操纵GlobalValue的linkage特性。 - Module *getParent()
- 这将返回当前嵌入这个GlobalValue的Module。
9.8 Function类
#include “llvm/IR/Function.h”
header source: Function.h
doxygen info: Function Class
Superclasses: GlobalValue, Constant, User, Value
Function
类表示LLVM中的一个过程。它实际上是LLVM层次结构中比较复杂的类之一,因为它必须跟踪大量数据。Function类跟踪基本块列表、形式参数列表和符号表。
基本块列表是函数对象中最常用的部分。该列表强制函数中块的隐式排序,这指示代码将如何由后端布局。此外,第一个基本块是函数的隐式入口节点。在LLVM中显式地分支到这个初始块是不合法的。不存在隐式的退出节点,实际上一个函数可能有多个退出节点。如果BasicBlock列表是空的,这表明函数实际上是一个函数声明:函数的实际主体还没有被链接进来。
除了基本块列表之外,函数类还跟踪函数接收到的形式参数列表。这个容器管理参数节点的生存期,就像BasicBlock列表管理BasicBlock一样。
SymbolTable是一个很少使用的LLVM特性,只在必须按名称查找值时才使用。除此之外,符号表还用于内部,以确保函数体中指令、基本块或参数的名称之间没有冲突。
注意,函数是一个全局值,因此也是一个常量。函数的值是它的地址(链接后),它保证是常量。
9.8.1 Function类的重要Public成员
Function(const FunctionType *Ty, LinkageTypes Linkage, const std::string &N = "", Module* Parent = 0)
构造函数,用于在需要创建新函数来添加程序时使用。构造函数必须指定要创建的函数的类型以及函数应该具有哪种类型的链接。FunctionType参数指定函数的形式参数和返回值。同一个FunctionType值可用于创建多个函数。父参数指定定义函数的模块。如果提供了这个参数,函数将自动插入该模块的函数列表中。bool isDeclaration ()
返回函数是否定义了主体。如果函数是“外部的”,那么它就没有主体,因此必须通过与在不同翻译单元中定义的函数链接来解决。Function::iterator
—— 基本块列表迭代器的类型定义
Function::const_iterator
—— const_iterator的类型定义。
begin(), end(), size(), empty()
这些转发方法使访问函数对象的BasicBlock列表的内容变得很容易。Function::BasicBlockListType &getBasicBlockList()
返回BasicBlock列表。当您需要更新列表或执行没有转发方法的复杂操作时,这是必需的。
Function::arg_iterator —— 参数列表iterator的类型定义
Function::const_arg_iterator —— const_iterator的类型定义。
arg_begin(), arg_end(), arg_size(), arg_empty()
这些转发方法使访问函数对象的参数列表的内容变得很容易。Function::ArgumentListType &getArgumentList()
返回参数列表。当您需要更新列表或执行没有转发方法的复杂操作时,这是必需的。BasicBlock &getEntryBlock()
返回函数的入口BasicBlock。因为函数的入口块总是第一个块,所以它返回Function的第一个块。Type *getReturnType()
FunctionType *getFunctionType()
它遍历Function的Type并返回函数的返回类型,或实际函数的FunctionType。SymbolTable *getSymbolTable()
返回指向此函数的SymbolTable的指针。
9.9 GlobalVariable类
#include “llvm/IR/GlobalVariable.h”
header source: GlobalVariable.h
doxygen info: GlobalVariable Class
Superclasses: GlobalValue, Constant, User, Value
全局变量用GlobalVariable
类表示。与函数一样,GlobalVariable
也是GlobalValue
的子类,因此总是由它们的地址引用(全局值必须保存在内存中,所以它们的“name”指的是它们的常量地址)。有关更多信息,请参见GlobalValue。全局变量可能有一个初值(它必须是一个Constant),如果它们有一个initializer,它们可能被标记为“常量”本身(表明它们的内容在运行时从不更改)。
9.9.1 GlobalVariable类的重要Public成员
GlobalVariable(const Type *Ty, bool isConstant, LinkageTypes &Linkage, Constant *Initializer = 0, const std::string &Name = "", Module* Parent = 0)
创建指定类型的一个新全局变量。如果isConstant
为真,那么全局变量将被标记为程序不变。Linkage
参数指定变量的linkage类型(internal, external, weak, linkonce, appending)。如果linkage是InternalLinkage、WeakAnyLinkage、WeakODRLinkage、LinkOnceAnyLinkage或LinkOnceODRLinkage,则生成的全局变量将具有internal linkage。AppendingLinkage将变量的所有实例(在不同的转换单元中)连接到一个变量中,但只适用于数组。有关linkage类型的详细信息,请参阅LLVM语言参考。也可以为全局变量指定一个初始化器、一个名称和要放入变量的模块。bool isConstant() const
如果这是一个已知不能在运行时修改的全局变量,则返回true。bool hasInitializer()
如果这个GlobalVariable有一个初始化器,则返回true。Constant *getInitializer()
返回一个GlobalVariable的初始值。如果没有初始化器,则调用此方法是不合法的。
9.10 BasicBlock类
#include “llvm/IR/BasicBlock.h”
header source: BasicBlock.h
doxygen info: BasicBlock Class
Superclass: Value
该类表示代码的单个入口和单个出口部分,编译器社区通常将其称为基本块。BasicBlock
类维护一个Instructions列表,这些指令构成了块的主体。与语言定义匹配,此指令列表的最后一个元素始终是一个终止符指令。
除了跟踪组成块的指令列表外,BasicBlock类还跟踪它所嵌入的Function。
注意,BasicBlocks本身是Values,因为它们由branches之类的指令引用,所以可以放在switch表中。BasicBlocks有类型label。
9.10.1 BasicBlock类的重要Public成员
BasicBlock(const std::string &Name = "", Function *Parent = 0)
BasicBlock
构造函数用于创建用于插入函数的新基本块。构造函数可选地接受新块的一个名称和将其插入其中的一个Function。如果指定了Parent
参数,则在指定Function的末尾自动插入新的BasicBlock;如果没有指定,则必须手动将BasicBlock插入Function。BasicBlock::iterator
—— 指令列表iterator的类型定义
BasicBlock::const_iterator
—— const_iterator的类型定义。
用于访问指令列表的begin(), end(), front(), back(), size(), empty()
STL样式函数。
这些方法和typedefs
是转发函数,它们具有与相同名称的标准库方法相同的语义。这些方法以易于操作的方式公开基本块的底层指令列表。要获得完整的容器操作(包括更新列表的操作),必须使用getInstList()
方法。BasicBlock::InstListType &getInstList()
此方法用于访问实际包含指令的底层容器。当BasicBlock类中没有要执行的操作的转发函数时,必须使用此方法。因为没有用于“更新”操作的转发函数,所以如果想更新BasicBlock的内容,就需要使用这个函数。Function *getParent()
返回一个指针,它指向这个块所嵌套的Function,或者返回一个空指针(如果它是无家可归的)。Instruction *getTerminator()
返回一个指向出现在BasicBlock末尾的终止符指令的指针。如果没有终止符指令,或者如果块中的最后一条指令不是终止符,则返回一个空指针。
9.11 Argument类
这个Value
的子类为函数的传入形式参数定义接口。一个函数维护其一个形式参数的列表。一个参数有一个指向父Function的指针。