LLVM代码中的RTTI替换


文档摘要

我们将从LLVM代码中的RTTI替换开始,并讨论其实现方式。然后,继续讨论基本容器和智能指针。最后,将探讨一些用于表示token位置的重要类,以及如何在Clang中实现诊断。稍后,在第4.6节"Clang插件项目"中,将使用这些类中的某些类在测试项目中。 LLVM由于性能问题而避免使用RTTI。LLVM引入了一些辅助函数,这些函数替换了RTTI对应物,允许从一个类型转换到另一个类型。基本函数如下: llvm::isa\ 类似于Java的instanceof运算符,根据测试对象是否属于测试类而返回true或false。 llvm::cast\ :当确定对象是指定的派生类型时,使用此强制转换运算符。如果转换失败(即对象不是预期类型),llvm::cast将终止程序。

我们将从LLVM代码中的RTTI替换开始,并讨论其实现方式。然后,继续讨论基本容器和智能指针。最后,将探讨一些用于表示token位置的重要类,以及如何在Clang中实现诊断。稍后,在第4.6节"Clang插件项目"中,将使用这些类中的某些类在测试项目中。

LLVM由于性能问题而避免使用RTTI。LLVM引入了一些辅助函数,这些函数替换了RTTI对应物,允许从一个类型转换到另一个类型。基本函数如下:

  • llvm::isa<>类似于Java的instanceof运算符,根据测试对象是否属于测试类而返回true或false。

  • llvm::cast<>:当确定对象是指定的派生类型时,使用此强制转换运算符。如果转换失败(即对象不是预期类型),llvm::cast将终止程序。只有在确信转换不会失败时才使用。

  • llvm::dyn_cast<>:这可能是LLVM中最常使用的强制转换运算符。llvm::dyn_cast用于当预期转换通常会成功,但存在一些不确定性时进行安全的向下转换。如果对象不是指定的派生类型,llvm::dyn_cast<>返回nullptr。

强制转换运算符不接受nullptr作为输入,但有两个特殊的强制转换运算符可以处理nullptr:

  • llvm::cast_if_present<>:llvm::cast<>的变体,接受nullptr值

  • llvm::dyn_cast_if_present<>:llvm::dyn_cast<>的变体,接受nullptr值

这两个运算符都可以处理nullptr值。如果输入为nullptr或转换失败,会简单地返回nullptr。

重要提示
强制转换运算符llvm::cast_if_present<>和llvm::dyn_cast_if_present<>最近被引入,具体是在2022年。是流行的运算符llvm::cast_or_null<>和llvm::dyn_cast_or_null<>的替代品,这些运算符最近仍在使用。旧版本仍然受支持,现在将调用重定向到新的强制转换运算符。有关此更改的更多信息,请参见讨论:https://discourse.llvm.org/t/psa-swapping-out-or-null-with-if-present/65018

如何在没有RTTI的情况下执行动态转换操作?这可以通过某些特定的装饰来完成,如一个简单的例子所示,该例子受启发于如何为你的类层次结构设置LLVM风格的RTTI1。将从基类clangbook::Animal开始,有两个派生类:clangbook::Horse和clangbook::Sheep。每匹马都可以根据其速度(以英里/小时为单位)进行分类,而每只羊则可以根据其羊毛重量进行分类:

void testAnimal() auto AnimalPtr =
std::make_unique<clangbook::Horse>(10); if
(llvm::isa<clangbook::Horse>(AnimalPtr)) llvm::outs() << "Animal is
a Horse and the horse speed is: " <<
llvm::dyn_cast<clangbook::Horse>(AnimalPtr.get())->getSpeed() <<
"mph "; else llvm::outs() << "Animal is not a Horse";

图4.2:LLVM isa<>和dyn_cast<>使用示例

这段代码应该产生以下输出:

Animal is a Horse and the horse speed is: 10mph

图4.2中的第48行展示了llvm::isa<>的使用,而第51行展示了llvm::dyn_cast<>的使用。在后一种情况下,将基类转换为clangbook::Horse并调用该类特有的方法。

看看类的实现,这将为我们提供关于RTTI替换工作原理的见解。将从基类clangbook::Animal开始:

class Animal

public: enum AnimalKind AK_Horse, AK_Sheep ;

public: Animal(AnimalKind K) : Kind(K); AnimalKind getKind() const
return Kind;

private: const AnimalKind Kind;

;

图4.3:clangbook::Animal类

代码中最重要的部分是第11行,指定了不同类型的动物。一个枚举值用于马(AK_Horse),另一个用于羊(AK_Sheep)。clangbook::Horse和clangbook::Sheep类的实现可以在以下代码中找到:

class Horse : public Animal

public: Horse(int S) : Animal(AK_Horse), Speed(S);

static bool classof(const Animal *A) return A->getKind() == AK_Horse;

int getSpeed() return Speed;

private: int Speed;

;

class Sheep : public Animal

public: Sheep(int WM) : Animal(AK_Sheep), WoolMass(WM);

static bool classof(const Animal *A) return A->getKind() == AK_Sheep;

int getWoolMass() return WoolMass;

private: int WoolMass;

;

图4.4:clangbook::Horse和clangbook::Sheep类

第25行和第37行特别重要,包含classof静态方法的实现。这个方法对于LLVM中的强制转换运算符至关重要。一个实现可能如下所示(简化版本):

template <typename To, typename From> bool isa(const From *Val)
return To::classof(Val);

图4.5:简化版本的llvm::isa<>实现

同样的机制可以应用于其他强制转换运算符。

我们接下来将讨论各种类型的容器,是STL对应物的更强大的替代品。

LLVM
ADT(抽象数据类型)库提供了一系列容器。其中一些是LLVM特有的,而其他一些可以视为STL容器的替代品。接下来,我们将探索一些由ADT提供的最流行的类。

标准C++库中用于处理字符串的主要类是std::string。尽管这个类设计为通用,但它存在一些与性能相关的问题。一个重要的问题是复制操作。由于在编译器中复制字符串是一个常见的操作,LLVM引入了一个专门的类,llvm::StringRef,可以有效地处理这个操作,而无需使用额外的内存。这个类与C++172中的
和C++203中的std::spanstd::span类似。

llvm::StringRef类维护对数据的引用,不需要像传统的C/C++字符串那样以空字符结束。基本上持有指向数据块的指针和块的大小,使得对象的有效大小为16字节。由于llvm::StringRef保留引用而不是实际数据,必须从一个现有的数据源构造。这个类可以从基本字符串对象如const
char*、std::string和std::string_view实例化。默认构造函数创建一个空对象,llvm::StringRef的典型使用示例如图4.6所示:

#include "llvm/ADT/StringRef.h" ... llvm::StringRef StrRef("Hello,
LLVM!"); // Efficient substring, no allocations llvm::StringRef SubStr =
StrRef.substr(0, 5);

llvm::outs() << "Original StringRef: " << StrRef.str() << "";
llvm::outs() << "Substring: " << SubStr.str() << "";

图4.6:llvm::StringRef使用示例

代码的输出如下:

Original StringRef: Hello, LLVM! Substring: Hello

LLVM中用于字符串操作的另一个类是llvm::Twine,特别适合于将多个对象拼接成一个。llvm::Twine类的典型使用示例如图4.7所示:

#include "llvm/ADT/Twine.h" ... llvm::StringRef Part1("Hello, ");
llvm::StringRef Part2("Twine!"); llvm::Twine Twine = Part1 + Part2; //
Efficient concatenation

// Convert twine to a string (actual allocation happens here)
std::string TwineStr = Twine.str(); llvm::outs() << "Twine result: "
<< TwineStr << "";

图4.7:llvm::Twine使用示例

代码的输出如下:

Twine result: Hello, Twine!

另一个广泛用于字符串操作的类是llvm::SmallString<>,表示一个栈上分配的字符串,大小固定,也可以超过这个大小,此时它会分配堆内存。这是栈分配空间效率和堆分配灵活性的结合。

llvm::SmallString<>的优势在于,在编译器任务中,字符串往往很小,适合栈上分配的空间。这避免了动态内存分配的开销。但在需要更大字符串的情况下,llvm::SmallString仍然可以容纳,过渡到堆内存。llvm::SmallString<>的典型使用示例如图4.8所示:

#include "llvm/ADT/SmallString.h" ... // Stack allocate space for up to
20 characters. llvm::SmallString<20> SmallStr;

// No heap allocation happens here. SmallStr = "Hello, "; SmallStr +=
"LLVM!";

llvm::outs() << "SmallString result: " << SmallStr << "";

图4.8:llvm::SmallString<>使用示例

尽管字符串操作是编译器任务(如文本解析)中的关键,LLVM还有许多其他辅助类。我们接下来将探索它的顺序容器。

LLVM推荐了一些标准库中的array和vector的优化替代品:

  • llvm::ArrayRef<>:
    一个辅助类,设计用于接受顺序列表元素的接口,仅用于只读访问。这个类类似于llvm::StringRef<>,因为它不拥有底层数据,只是引用它。

  • llvm::SmallVector<>:
    对于小尺寸情况的优化向量。类似于第4.3.2节中讨论的llvm::SmallString,特别是,数组的大小不是固定的,允许存储的元素数量增长。如果元素数量保持在N(模板参数)以下,则无需进行额外的内存分配

更好地理解这些容器,如图4.9所示:

llvm::SmallVector<int, 10> SmallVector; for (int i = 0; i < 10; i++)
SmallVector.push_back(i); SmallVector.push_back(10);

图4.9:llvm::SmallVector<>的使用

该vector在Line
1以选择的10(由第二个模板参数指示)大小初始化。该容器提供了类似于std::vector<>的API,使用熟悉的push_back方法添加新元素,如图4.9,Lines
3和5所示。

前10个元素被添加到vector中,而没有进行额外的内存分配(见图4.9,Lines
2-4)。当在第5行添加第11个元素时,数组的大小超过了为10个元素预分配的空间,触发额外的内存分配。这种容器设计有效地最小化了小对象的内存分配,同时保持了在必要时容纳更大尺寸的灵活性。

标准库提供了几种用于存储键值数据的容器,如std::map<>用于通用映射和std::unordered_map<>用于哈希映射。LLVM提供了这些标准容器的替代:

  • llvm::StringMap<>:
    使用字符串作为键的映射,这比标准的关联容器std::unordered_map<std::string,
    T>更性能优化。在字符串键占主导地位,且性能至关重要的情况下经常使用,例如在LLVM这样的编译器基础设施中。与LLVM中的许多其他数据结构不同,llvm::StringMap<>不存储字符串键的副本。相反,它保留对字符串数据的引用,因此必须确保字符串数据在映射之外生存,以防止未定义的行为。

  • llvm::DenseMap<>:
    这个map设计在大多数情况下比std::unordered_map<>更节省内存和时间,尽管它有一些约束(例如,键和值具有平凡的析构函数)。当有简单的键值类型并需要高性能查找时,特别适用。

  • llvm::SmallDenseMap<>:
    这个映射类似于llvm::DenseMap<>,但针对通常大小较小的实例进行了优化。它为小映射栈分配,并且只在映射超过预定义的大小时才求助于堆分配。

  • llvm::MapVector<>:
    这个容器保留了插入顺序,类似于Python的OrderedDict。它实现为std::vector和llvm::DenseMap或llvm::SmallDenseMap的混合体。

这些容器利用了二次探查哈希表机制。这种方法在解决哈希冲突时有效,因为在元素查找期间不需要重新计算缓存。这对于性能关键的应用程序至关重要。

LLVM代码中可以找到不同的智能指针。最受欢迎的来自标准模板库:std::unique_ptr<>和std::shared_ptr<>。此外,LLVM还提供了一些辅助类来使用智能指针。其中最著名的是llvm::IntrusiveRefCntPtr<>。这个智能指针设计用于与支持侵入式引用计数的对象一起工作。与std::shared_ptr不同,后者维护自己的控制块来管理引用计数,IntrusiveRefCntPtr期望对象维护自己的引用计数。这种设计可能更节省内存。一个使用示例如下所示:

class MyClass : public llvm::RefCountedBase<MyClass> // ... ;

llvm::IntrusiveRefCntPtr<MyClass> Ptr = new MyClass();

图4.10:llvm::IntrusiveRefCntPtr<>使用示例

智能指针使用了CRTP(Curiously Recurring Template
Pattern),该模式在前述第3.3节"AST遍历"中提到。CRTP当引用计数降至0时,必须释放对象。实现如下:

template <class Derived> class RefCountedBase // ... void Release()
const assert(RefCount > 0 && "Reference count is already zero."); if
(–RefCount == 0) delete static_cast<const Derived *>(this);

图4.11:llvm::RefCountedBase<>中CRTP的使用。代码来源于llvm/ADT/IntrusiveRefCntPtr.h头文件

由于图4.10中的MyClass是从RefCountedBase派生的,可以在图4.11的Line
6上对其进行类型转换。由于转换的类型已知,其是作为模板参数提供的,因此这种转换可行。

我们刚刚完成了LLVM基本库的学习,现在该转向Clang基本库了。Clang是一个编译器前端,其最重要的操作与诊断相关。诊断需要精确的源代码位置信息,让我们探索Clang为这些操作提供的基本类。

  1. LLVM Community. How to set up LLVM-style RTTI for your class
    hierarchy. 2023. URL
    https://llvm.org/docs/HowToSetUpLLVMStyleRTTI.html

  2. International Organization for Standardization. International
    Standard ISO/IEC 14882:2017(E) – Programming Languages – C++.
    International Organization for Standardization, 2017. URL
    https://www.iso.org/standard/69466.html

  3. International Organization for Standardization. International
    Standard ISO/IEC 14882:2020(E) – Programming Languages – C++.
    International Organization for Standardization, 2020. URL
    https://www.iso.org/standard/73560.html


发布者: 作者: 转发
评论区 (0)
U