1 TableGen 后端开发者指南

1.1 简介

TableGen 的目的是基于源文件中的信息生成复杂的输出文件,这些源文件比输出文件更容易编写代码,并且也更容易长期维护和修改。 这些信息以声明式风格编码,涉及类和记录,然后由 TableGen 处理。 内部化的记录被传递给各种后端,后端从记录的子集中提取信息并生成输出文件。 这些输出文件通常是 C++ 的 .inc 文件,但也可能是后端开发者需要的任何类型的文件。

本文档是编写 TableGen 后端的指南。 它不是完整的参考手册,而是使用 TableGen 为后端提供的工具的指南。 有关所涉及的各种数据结构和函数的完整参考,请参阅主要的 TableGen 头文件 (record.h) 和/或 Doxygen 文档。

本文档假设您已阅读 TableGen 程序员参考手册,其中提供了 TableGen 源代码文件编码的详细参考。 有关现有后端的描述,请参阅 TableGen 后端

1.2 数据结构

以下章节描述了数据结构,这些数据结构包含由 TableGen 解析器从 TableGen 源代码文件收集的类和记录。 请注意,术语指的是抽象记录类,而术语记录指的是具体记录。

除非另有说明,否则与类关联的函数是实例函数。

1.2.1 RecordKeeper

RecordKeeper 类的实例充当 TableGen 解析和收集的所有类和记录的容器。 当后端被 TableGen 调用时,RecordKeeper 实例会传递给后端。 此类通常缩写为 RK

记录管理器中有两个映射,一个用于类,一个用于记录(后者通常称为 defs)。 每个映射将类或记录名称映射到 Record 类的实例(参见 Record),其中包含有关该类或记录的所有信息。

除了这两个映射之外,RecordKeeper 实例还包含

  • 一个映射,将全局变量的名称映射到它们的值。 全局变量在 TableGen 文件中使用外部 defvar 语句定义。

  • 用于命名匿名记录的计数器。

RecordKeeper 类提供了一些有用的函数。

  • 获取完整的类和记录映射的函数。

  • 获取基于其父类的记录子集的函数。

  • 通过名称获取单个类、记录和全局变量的函数。

RecordKeeper 实例可以使用 << 运算符打印到输出流。

1.2.2 Record

由 TableGen 构建的每个类或记录都由 Record 类的实例表示。 RecordKeeper 实例包含一个用于类,一个用于记录的映射。 记录的主要数据成员是记录名称、字段名称及其值的向量,以及记录的超类向量。

记录名称存储为指向 Init 的指针(参见 Init),Init 是一个类,其实例保存 TableGen 值(有时称为初始化器)。 字段名称和值存储在 RecordVal 实例向量中(参见 RecordVal),每个实例都包含字段名称及其值。 超类向量包含一系列对,每对包括超类记录及其源文件位置。

除了这些成员之外,Record 实例还包含

  • 源文件位置向量,包括记录定义本身,以及其定义中涉及的任何多重类的位置。

  • 对于类记录,类的模板参数向量。

  • 与此记录对应的 DefInit 实例(参见 DefInit)。

  • 唯一的记录 ID。

  • 一个布尔值,指定这是否是类定义。

  • 一个布尔值,指定这是否是匿名记录。

Record 类提供了许多有用的函数。

  • 获取记录名称、字段、源文件位置、模板参数和唯一 ID 的函数。

  • 获取记录的所有超类或仅获取其直接超类的函数。

  • 通过以各种形式指定其名称并以各种形式返回其值来获取特定字段值的函数(参见 获取记录名称和字段)。

  • 检查记录各种属性的布尔函数。

Record 实例可以使用 << 运算符打印到输出流。

1.2.3 RecordVal

记录的每个字段都存储在 RecordVal 类的实例中。 Record 实例包含这些值实例的向量。 RecordVal 实例包含字段的名称,存储在 Init 实例中。 它还包含字段的值,同样存储在 Init 中。 (这个类更好的名称可能是 RecordField。)

除了这些主要成员之外,RecordVal 还有其他数据成员。

  • 字段定义的源文件位置。

  • 字段的类型,存储为 RecTy 类的实例(参见 RecTy)。

RecordVal 类提供了一些有用的函数。

  • 以各种形式获取字段名称的函数。

  • 获取字段类型的函数。

  • 获取字段值的函数。

  • 获取源文件位置的函数。

请注意,字段值更容易直接从 Record 实例中获取(参见 Record)。

RecordVal 实例可以使用 << 运算符打印到输出流。

1.2.4 RecTy

RecTy 类用于表示字段值的类型。 它是子类系列的基础类,每个子类对应一个可用的字段类型。 RecTy 类有一个数据成员,它是一个枚举类型,用于指定字段值的特定类型。 (这个类更好的名称可能是 FieldTy。)

RecTy 类提供了一些有用的函数。

  • 获取类型名称作为字符串的虚函数。

  • 检查此类型的所有值是否可以转换为另一个给定类型的虚函数。

  • 检查此类型是否是另一个给定类型的子类型的虚函数。

  • 获取具有此类型元素的列表的相应 list 类型的函数。 例如,当使用 int 类型调用时,该函数返回 list<int> 类型。

RecTy 继承的子类有 BitRecTyBitsRecTyCodeRecTyDagRecTyIntRecTyListRecTyRecordRecTyStringRecTy。 其中一些类有其他成员,将在以下小节中描述。

所有RecTy 派生的类都提供 get() 函数。 它返回与派生类对应的 Recty 实例。 某些 get() 函数需要一个参数来指定所需的类型的特定变体。 这些参数在以下小节中描述。

RecTy 实例可以使用 << 运算符打印到输出流。

警告

未指定特定类型的 RecTy 实例是单个实例还是多个实例。

1.2.4.1 BitsRecTy

此类包含一个数据成员,其中包含 bits 值的大小以及获取该大小的函数。

get() 函数接受序列的长度 n,并返回与 bits<n> 对应的 BitsRecTy 类型。

1.2.4.2 ListRecTy

此类包含一个数据成员,用于指定列表元素的类型以及获取该类型的函数。

get() 函数接受列表成员的 RecTy type,并返回与 list<type> 对应的 ListRecTy 类型。

1.2.4.3 RecordRecTy

此类包含数据成员,其中包含此记录的父类列表。 它还提供了一个函数来获取类数组,以及两个函数来获取迭代器 begin()end() 值。 类为后两个函数的返回值定义了一个类型。

using const_record_iterator = Record * const *;

get() 函数接受指向记录的直接超类的 Record 实例的指针的 ArrayRef,并返回与从这些超类继承的记录对应的 RecordRecTy

1.2.5 Init

Init 类用于表示 TableGen 值。 名称源自初始化值。 此类不应与 RecordVal 类混淆,后者表示记录字段,包括其名称和值。 Init 类是一系列子类的基类,每个子类对应一个可用的值类型。 Init 的主要数据成员是一个枚举类型,表示值的特定类型。

Init 类提供了一些有用的函数。

  • 获取类型枚举器的函数。

  • 布尔虚函数,用于确定值是否完全指定;即,没有未初始化的子值。

  • 将值作为字符串获取的虚函数。

  • 将值转换为其他类型、实现 TableGen 的位范围功能和实现列表切片功能的虚函数。

  • 获取值的特定位的虚函数。

直接从 Init 继承的子类是 UnsetInitTypedInit

Init 实例可以使用 << 运算符打印到输出流。

警告

未指定具有相同底层类型和值的两个单独的初始化值(例如,两个值为“Hello”的字符串)是由两个 Init 表示还是共享相同的 Init

1.2.5.1 UnsetInit

此类是 Init 的子类,表示未设置(未初始化)的值。 静态函数 get() 可用于获取此类型的单例 Init

1.2.5.2 TypedInit

此类是 Init 的子类,充当表示特定值类型(未设置值除外)的类的父类。 这些类包括 BitInitBitsInitDagInitDefInitIntInitListInitStringInit。 (TableGen 解析器使用了其他派生类型。)

此类包含一个数据成员,用于指定值的 RecTy 类型。 它提供了一个函数来获取该 RecTy 类型。

1.2.5.3 BitInit

BitInit 类是 TypedInit 的子类。 它的实例表示位的可能值:0 或 1。 它包含一个数据成员,其中包含位。

所有TypedInit 派生的类都提供以下函数。

  • 名为 get() 的静态函数,返回表示指定值的 Init。 在 BitInit 的情况下,get(true) 返回表示 true 的 BitInit 实例,而 get(false) 返回表示 false 的实例。 如上所述,未指定是否存在一个或多个表示 true(或 false)的 BitInit

  • 名为 GetValue() 的函数,以更直接的形式返回实例的值,在这种情况下为 bool

1.2.5.4 BitsInit

BitsInit 类是 TypedInit 的子类。 它的实例表示位序列,从高位到低位。 它包含一个数据成员,其中包含序列的长度和一个指向 Init 实例的指针向量,每位一个。

类提供了常用的 get() 函数。 它不提供 getValue() 函数。

类提供了以下附加函数。

  • 获取序列中位数的函数。

  • 获取由整数索引指定的位的函数。

1.2.5.5 DagInit

DagInit 类是 TypedInit 的子类。 它的实例表示可能的有向无环图 (dag)。

类包含指向 DAG 运算符的 Init 指针和指向运算符名称的 StringInit 指针。 它包括 DAG 操作数的计数和操作数名称的计数。 最后,它包括一个指向操作数的 Init 实例的指针向量和另一个指向操作数名称的 StringInit 实例的向量。 (DAG 操作数也称为参数。)

类提供了常用 get() 函数的两种形式。 它不提供常用的 getValue() 函数。

类提供了许多附加函数

  • 以各种形式获取运算符和以各种形式获取运算符名称的函数。

  • 确定是否存在任何操作数并获取操作数数量的函数。

  • 获取操作数的函数,包括单个和一起获取。

  • 确定是否存在任何名称并获取名称数量的函数

  • 获取名称的函数,包括单个和一起获取。

  • 获取操作数迭代器 begin()end() 值的函数。

  • 获取名称迭代器 begin()end() 值的函数。

类为操作数和名称迭代器的返回值定义了两种类型。

using const_arg_iterator = SmallVectorImpl<Init*>::const_iterator;
using const_name_iterator = SmallVectorImpl<StringInit*>::const_iterator;

1.2.5.6 DefInit

DefInit 类是 TypedInit 的子类。 它的实例表示由 TableGen 收集的记录。 它包含一个数据成员,该成员是指向记录的 Record 实例的指针。

类提供了常用的 get() 函数。 它不提供 getValue()。 相反,它提供了 getDef(),它返回 Record 实例。

1.2.5.7 IntInit

IntInit 类是 TypedInit 的子类。 它的实例表示 64 位整数的可能值。 它包含一个数据成员,其中包含整数。

类提供了常用的 get()getValue() 函数。 后一个函数将整数作为 int64_t 返回。

类还提供了一个函数 getBit(),用于获取整数值的指定位。

1.2.5.8 ListInit

ListInit 类是 TypedInit 的子类。 它的实例表示某种类型的元素列表。 它包含一个数据成员,其中包含列表的长度和一个指向 Init 实例的指针向量,每个元素一个。

此类提供了常用的 get()getValues() 函数。 后一个函数返回指向 Init 实例的指针向量的 ArrayRef

此类提供了这些附加功能。

  • 一个用于获取元素类型的函数。

  • 用于获取向量长度和确定向量是否为空的函数。

  • 用于获取由整数索引指定的元素并以各种形式返回它的函数。

  • 用于获取迭代器 begin()end() 值的函数。 此类为这两个函数的返回类型定义了一个类型。

using const_iterator = Init *const *;

1.2.5.9 StringInit

StringInit 类是 TypedInit 的子类。 它的实例表示任意长度的字符串。 它包含一个数据成员,其中包含值的 StringRef

此类提供了常用的 get()getValue() 函数。 后一个函数返回 StringRef

1.3 创建新的后端

创建 TableGen 的新后端需要以下步骤。

  1. 为您的后端 C++ 文件创建一个名称,例如 GenAddressModes

  2. 编写新的后端,使用文件 TableGenBackendSkeleton.cpp 作为起点。

  3. 确定哪个 TableGen 实例需要新的后端。 Clang 有一个实例,LLVM 也有一个实例。 或者您可能正在构建自己的实例。

  4. 将您的后端 C++ 文件添加到相应的 CMakeLists.txt 文件中,以便构建它。

  5. 将您的 C++ 文件添加到系统。

1.4 后端骨架

文件 TableGenBackendSkeleton.cpp 为编写新的 TableGen 后端提供了骨架 C++ 翻译单元。 以下是关于该文件的一些注释。

  • 包含列表是大多数后端所需的最小列表。

  • 与所有 LLVM C++ 文件一样,它具有 using namespace llvm; 语句。 它还具有一个匿名命名空间,其中包含所有特定于文件的数据结构定义,以及体现发射器数据成员和函数的类。 继续使用 GenAddressModes 示例,此类命名为 AddressModesEmitter

  • 发射器类的构造函数接受 RecordKeeper 引用,通常命名为 RKRecordKeeper 引用保存在数据成员中,以便可以从中获取记录。 此数据成员通常命名为 Records

  • 一个函数命名为 run。 它由后端的 “main 函数” 调用,以收集记录并发出输出文件。 它接受 raw_ostream 类的实例,通常命名为 OS。 通过写入此流来发出输出文件。

  • run 函数应使用 emitSourceFileHeader 辅助函数在发出的文件中包含标准标头。

  • 使用 llvm/TableGen/TableGenBackend.h 将类或函数注册为命令行选项。

    • 如果类具有构造函数 (RK) 和方法 run(OS),请使用 llvm::TableGen::Emitter::OptClass<AddressModesEmitter>

    • 否则,请使用 llvm::TableGen::Emitter::Opt

本文档其余部分中的所有示例都将假定骨架文件中使用的命名约定。

1.5 获取类

RecordKeeper 类提供了两个函数,用于获取 TableGen 文件中定义的类的 Record 实例。

  • getClasses() 返回所有类的 RecordMap 引用。

  • getClass(name) 返回命名类的 Record 引用。

如果您需要迭代所有类记录

for (auto ClassPair : Records.getClasses()) {
  Record *ClassRec = ClassPair.second.get();
  ...
}

ClassPair.second 获取类的 unique_ptr,然后 .get() 获取类 Record 本身。

1.6 获取记录

RecordKeeper 类提供了四个函数,用于获取 TableGen 文件中定义的具体记录的 Record 实例。

  • getDefs() 返回所有具体记录的 RecordMap 引用。

  • getDef(name) 返回命名具体记录的 Record 引用。

  • getAllDerivedDefinitions(classname) 返回从给定类派生的具体记录的 Record 引用向量。

  • getAllDerivedDefinitions(classnames) 返回从所有给定类派生的具体记录的 Record 引用向量。

此语句获取从 Attribute 类派生的所有记录,并对其进行迭代。

auto AttrRecords = Records.getAllDerivedDefinitions("Attribute");
for (Record *AttrRec : AttrRecords) {
  ...
}

1.7 获取记录名称和字段

如上所述(请参阅 Record),有多个函数返回记录的名称。 一个特别有用的函数是 getNameInitAsString(),它将名称作为 std::string 返回。

还有多个函数返回记录的字段。 要获取和迭代所有字段

for (const RecordVal &Field : SomeRec->getValues()) {
  ...
}

您会记得 RecordVal 是其实例包含有关记录中字段信息的类。

getValue() 函数返回由名称指定的字段的 RecordVal 实例。 有多个重载函数,一些采用 StringRef,另一些采用 const Init *。 一些函数返回 RecordVal *,另一些函数返回 const RecordVal *。 如果字段不存在,则会打印致命错误消息。

通常,您对字段的值感兴趣,而不是 RecordVal 中的所有信息。 有大量函数以某种形式获取字段名称并返回其值。 一个函数 getValueInit 将值作为 Init * 返回。 另一个函数 isValueUnset 返回一个布尔值,指定值是否未设置(未初始化)。

大多数函数以更有用的形式返回值。 例如

std::vector<int64_t> RegCosts =
    SomeRec->getValueAsListOfInts("RegCosts");

假定字段 RegCosts 是整数列表。 该列表作为 64 位整数的 std::vector 返回。 如果该字段不是整数列表,则会打印致命错误消息。

这是一个将字段值作为 Record 返回的函数,但如果该字段不存在,则返回 null。

if (Record *BaseRec = SomeRec->getValueAsOptionalDef(BaseFieldName)) {
  ...
}

假定该字段将另一个记录作为其值。 该记录作为指向 Record 的指针返回。 如果该字段不存在或未设置,则该函数返回 null。

1.8 获取记录超类

Record 类提供了一个函数来获取记录的超类。 它名为 getSuperClasses,并返回 std::pair 对数组的 ArrayRef。 超类按后序排列:复制其字段到记录时访问超类的顺序。 每对都包含指向超类记录的 Record 实例的指针和 SMRange 类的实例。 该范围指示类定义的开始和结束位置的源文件位置。

此示例获取 Prototype 记录的超类,然后迭代返回数组中的对。

ArrayRef<std::pair<const Record *, SMRange>>
    Superclasses = Prototype->getSuperClasses();
for (const auto &SuperPair : Superclasses) {
  ...
}

Record 类还提供了一个函数 getDirectSuperClasses,用于将记录的直接超类附加到给定类型的向量 SmallVectorImpl<Record *>

1.9 将文本发送到输出流

run 函数传递一个 raw_ostream,它将输出文件打印到该流。 按照惯例,此流保存在名为 OS 的发射器类成员中,尽管某些 run 函数很简单,只是使用该流而不保存它。 可以通过将值直接写入输出流,或使用 std::format()llvm::formatv() 函数来生成输出。

OS << "#ifndef " << NodeName << "\n";

OS << format("0x%0*x, ", Digits, Value);

可以使用 << 运算符打印以下类的实例:RecordKeeperRecordRecTyRecordValInit

辅助函数 emitSourceFileHeader() 打印应包含在每个输出文件顶部的标头注释。 对它的调用包含在骨架后端文件 TableGenBackendSkeleton.cpp 中。

1.10 打印错误消息

TableGen 记录通常从多个类派生,并且也经常通过一系列多类定义。 因此,后端可能难以报告具有准确源文件位置的清晰错误消息。 为了使错误报告更容易,提供了五个错误报告函数,每个函数都有四个重载。

  • PrintWarning 打印标记为警告的消息。

  • PrintError 打印标记为错误的消息。

  • PrintFatalError 打印标记为错误的消息,然后终止。

  • PrintNote 打印注释。 它通常在前一个函数之一之后使用,以提供更多信息。

  • PrintFatalNote 打印注释,然后终止。

这五个函数中的每一个都重载了四次。

  • PrintError(const Twine &Msg):打印没有源文件位置的消息。

  • PrintError(ArrayRef<SMLoc> ErrorLoc, const Twine &Msg):打印消息,后跟指定的源行,以及指向错误项的指针。 源文件位置数组通常从 Record 实例中获取。

  • PrintError(const Record *Rec, const Twine &Msg):打印消息,后跟与指定记录关联的源行(请参阅 Record)。

  • PrintError(const RecordVal *RecVal, const Twine &Msg):打印消息,后跟与指定记录字段关联的源行(请参阅 RecordVal)。

使用这些函数,目标是生成尽可能具体的错误报告。

1.11 调试工具

TableGen 提供了一些工具来帮助调试后端。

1.11.1 PrintRecords 后端

TableGen 命令选项 --print-records 调用一个简单的后端,该后端打印源文件中定义的所有类和记录。 这是默认的后端选项。 输出格式保证随时间保持不变,以便可以在测试中比较输出。 输出如下所示

------------- Classes -----------------
...
class XEntry<string XEntry:str = ?, int XEntry:val1 = ?> { // XBase
  string Str = XEntry:str;
  bits<8> Val1 = { !cast<bits<8>>(XEntry:val1){7}, ... };
  bit Val3 = 1;
}
...
------------- Defs -----------------
def ATable {  // GenericTable
  string FilterClass = "AEntry";
  string CppTypeName = "AEntry";
  list<string> Fields = ["Str", "Val1", "Val2"];
  list<string> PrimaryKey = ["Val1", "Val2"];
  string PrimaryKeyName = "lookupATableByValues";
  bit PrimaryKeyEarlyOut = 0;
}
...
def anonymous_0 {     // AEntry
  string Str = "Bob";
  bits<8> Val1 = { 0, 0, 0, 0, 0, 1, 0, 1 };
  bits<10> Val2 = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 };
}

类显示其模板参数、父类(在 // 之后)和字段。 记录显示其父类和字段。 请注意,匿名记录命名为 anonymous_0anonymous_1 等。

1.11.2 PrintDetailedRecords 后端

TableGen 命令选项 --print-detailed-records 调用一个后端,该后端打印源文件中定义的所有全局变量、类和记录。 输出格式保证随时间保持不变。 输出如下所示。

DETAILED RECORDS for file llvm-project\llvm\lib\target\arc\arc.td

-------------------- Global Variables (5) --------------------

AMDGPUBufferIntrinsics = [int_amdgcn_s_buffer_load, ...
AMDGPUImageDimAtomicIntrinsics = [int_amdgcn_image_atomic_swap_1d, ...
...
-------------------- Classes (758) --------------------

AMDGPUBufferLoad  |IntrinsicsAMDGPU.td:879|
  Template args:
    LLVMType AMDGPUBufferLoad:data_ty = llvm_any_ty  |IntrinsicsAMDGPU.td:879|
  Superclasses: (SDPatternOperator) Intrinsic AMDGPURsrcIntrinsic
  Fields:
    list<SDNodeProperty> Properties = [SDNPMemOperand]  |Intrinsics.td:348|
    string LLVMName = ""  |Intrinsics.td:343|
...
-------------------- Records (12303) --------------------

AMDGPUSample_lz_o  |IntrinsicsAMDGPU.td:560|
  Defm sequence: |IntrinsicsAMDGPU.td:584| |IntrinsicsAMDGPU.td:566|
  Superclasses: AMDGPUSampleVariant
  Fields:
    string UpperCaseMod = "_LZ_O"  |IntrinsicsAMDGPU.td:542|
    string LowerCaseMod = "_lz_o"  |IntrinsicsAMDGPU.td:543|
...
  • 使用外部 defvar 语句定义的全局变量与其值一起显示。

  • 类显示其源位置、模板参数、超类和字段。

  • 记录显示其源位置、defm 序列、超类和字段。

超类按处理顺序显示,间接超类用括号括起来。 每个字段都显示其值和设置它的源位置。defm 序列给出参与生成记录的 defm 语句的位置,按调用顺序排列。

1.11.3 TableGen 阶段计时

TableGen 提供了一个阶段计时功能,该功能生成一个报告,说明解析源文件和运行选定后端各个阶段所用的时间。 此功能通过 TableGen 命令的 --time-phases 选项启用。

如果后端针对计时进行检测,则会生成如下报告。 这是在 AMDGPU 目标上运行的 --print-detailed-records 后端的计时。

===-------------------------------------------------------------------------===
                             TableGen Phase Timing
===-------------------------------------------------------------------------===
  Total Execution Time: 101.0106 seconds (102.4819 wall clock)

   ---User Time---   --System Time--   --User+System--   ---Wall Time---  --- Name ---
  85.5197 ( 84.9%)   0.1560 ( 50.0%)  85.6757 ( 84.8%)  85.7009 ( 83.6%)  Backend overall
  15.1789 ( 15.1%)   0.0000 (  0.0%)  15.1789 ( 15.0%)  15.1829 ( 14.8%)  Parse, build records
   0.0000 (  0.0%)   0.1560 ( 50.0%)   0.1560 (  0.2%)   1.5981 (  1.6%)  Write output
  100.6986 (100.0%)   0.3120 (100.0%)  101.0106 (100.0%)  102.4819 (100.0%)  Total

请注意,后端的所有时间都集中在 “后端总体” 下。

如果后端已针对计时进行检测,则其处理将分为多个阶段,并且每个阶段分别计时。 这是在 AMDGPU 目标上运行的 --emit-dag-isel 后端的计时。

===-------------------------------------------------------------------------===
                             TableGen Phase Timing
===-------------------------------------------------------------------------===
  Total Execution Time: 746.3868 seconds (747.1447 wall clock)

   ---User Time---   --System Time--   --User+System--   ---Wall Time---  --- Name ---
  657.7938 ( 88.1%)   0.1404 ( 90.0%)  657.9342 ( 88.1%)  658.6497 ( 88.2%)  Emit matcher table
  70.2317 (  9.4%)   0.0000 (  0.0%)  70.2317 (  9.4%)  70.2700 (  9.4%)  Convert to matchers
  14.8825 (  2.0%)   0.0156 ( 10.0%)  14.8981 (  2.0%)  14.9009 (  2.0%)  Parse, build records
   2.1840 (  0.3%)   0.0000 (  0.0%)   2.1840 (  0.3%)   2.1791 (  0.3%)  Sort patterns
   1.1388 (  0.2%)   0.0000 (  0.0%)   1.1388 (  0.2%)   1.1401 (  0.2%)  Optimize matchers
   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0050 (  0.0%)  Write output
  746.2308 (100.0%)   0.1560 (100.0%)  746.3868 (100.0%)  747.1447 (100.0%)  Total

后端已分为四个阶段并分别计时。

如果要检测后端,请参阅后端 DAGISelEmitter.cpp 并搜索 Records.startTimer