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 解析并收集的所有类和记录的容器。RecordKeeper 实例在 TableGen 调用后端时传递给后端。此类通常缩写为 RK

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

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

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

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

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

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

  • 根据其父类获取记录子集的函数。

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

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

1.2.2 Record

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

记录名称存储为指向 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()函数接受列表成员的RecTytype作为参数,并返回对应于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()的静态函数,它返回一个表示指定值(s)的Init。对于BitInitget(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。后端的“主函数”调用它来收集记录并发出输出文件。它接受 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 获取记录名称和字段

如上所述(参见 记录),有多个函数可以返回记录的名称。其中一个特别有用的函数是 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 并返回一个 ArrayRef,其中包含一个 std::pair 对的数组。超类按后序排列:在将超类的字段复制到记录中时访问超类的顺序。每对都包含指向超类记录的 Record 实例的指针和 SMRange 类的实例。该范围指示类定义的开始和结束的源文件位置。

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

ArrayRef<std::pair<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):打印消息,后跟与指定记录关联的源代码行(参见 记录)。

  • 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