是什么使得 C + + RTTI 不受欢迎?

查看 LLVM 文档,他们提到了 他们使用“ RTTI 的自定义形式”,这就是他们拥有 isa<>cast<>dyn_cast<>模板函数的原因。

通常,读到库重新实现某种语言的某些基本功能是一种糟糕的代码味道,只会让人想要运行它。然而,这是我们正在谈论的 LLVM: 这些家伙正在开发一个 C + + 编译器 还有 a C + + 运行时。如果他们不知道他们在做什么,我就完蛋了,因为我更喜欢 clang而不是随 Mac OS 一起运行的 gcc版本。

尽管如此,由于没有他们那么有经验,我还是想知道正常 RTTI 的缺陷是什么。我知道它只适用于具有 v 表的类型,但这只引发了两个问题:

  • 既然您只需要一个虚方法来拥有一个 vtable,为什么他们不将一个方法标记为 virtual呢?虚拟毁灭者似乎很擅长这个。
  • 如果他们的解决方案没有使用常规的 RTTI,知道它是如何实现的吗?
17531 次浏览

LLVM 编码标准似乎很好地回答了这个问题:

为了减少代码和可执行文件的大小,LLVM 不使用 RTTI (例如 Dynamic _ cast < >)或异常。这两个语言特性违反了一般的 C + + 原则“你只为你使用的东西付费”,即使代码库中从来没有使用过异常,或者 RTTI 从来没有用于类,也会导致可执行的膨胀。因此,我们在代码中全局关闭它们。

也就是说,LLVM 确实广泛使用了手工卷制的 RTTI 形式,它使用了诸如 isa < > 、 cast < > 和 dyn _ cast < > 等模板。这种形式的 RTTI 是可选的,可以添加到任何类中。它也比 Dynamic _ cast < > 高效得多。

这里 是一篇关于 RTTI 的好文章,以及为什么您可能需要使用自己的版本。

我不是 C + + RTTI 方面的专家,但是我也实现了我自己的 RTTI,因为有明确的理由说明为什么需要这样做。首先,C + + RTTI 系统功能不是很丰富,基本上你所能做的就是类型转换和获取基本信息。如果在运行时,您有一个具有类名的字符串,并且您想要构造该类的一个对象,那么使用 C + + RTTI 来完成这个任务会怎么样呢。此外,C + + RTTI 不能真正(或容易)跨模块移植(您无法识别从另一个模块(dll/so 或 exe)创建的对象的类)。类似地,C + + RTTI 的实现是特定于编译器的,并且对于所有类型实现这个实现的额外开销而言,通常开启这个实现是昂贵的。最后,它不是真正持久的,所以它不能真正用于文件保存/加载(例如,你可能想要保存一个对象的数据到一个文件,但是你也想要保存它的类的“ typeid”,这样,在加载时,你知道要创建哪个对象来加载这个数据,这是不能用 C + + RTTI 可靠地完成的)。由于所有或其中一些原因,许多框架都有自己的 RTTI (从非常简单到特性非常丰富)。例如 wxWidget,LLVM,Boost。序列化等等。这种情况并不少见。

既然您只需要一个虚方法来拥有一个 vtable,为什么他们不直接将一个方法标记为虚方法呢?虚拟毁灭者似乎很擅长这个。

他们的 RTTI 系统可能也是这么用的。虚函数是动态绑定(运行时绑定)的基础,因此,它基本上是执行任何类型的运行时类型识别/信息所必需的(不仅是 C + + RTTI 所需要的,而且 RTTI 的任何实现都必须以这样或那样的方式依赖于虚调用)。

如果他们的解决方案没有使用常规的 RTTI,知道它是如何实现的吗?

当然,您可以在 C + + 中查找 RTTI 实现。我已经做了我自己的,有许多图书馆,有他们自己的 RTTI 以及。其实写起来很简单。基本上,你所需要的只是一种唯一表示类型的方法(比如类的名称,或者它的一些错误版本,甚至每个类的唯一 ID) ,一种类似于 type_info的结构,它包含了所有你需要的类型信息,然后你需要在每个类中有一个“隐藏”的虚函数,它会根据请求返回这个类型信息(如果这个函数在每个派生类中被重写,它就会工作)。当然,还有一些额外的事情可以做,比如所有类型的单例存储库,可能还有相关的工厂函数(这对于创建类型的对象非常有用,因为在运行时所知道的只是类型的名称,比如字符串或者类型 ID)。此外,您可能希望添加一些虚函数以允许动态类型强制转换(通常是通过调用最派生类的强制转换函数并将 static_cast执行到您希望强制转换的类型来实现)。

主要原因是他们努力保持尽可能低的内存使用。

RTTI 只适用于具有至少一个虚方法的类,这意味着类的实例将包含指向虚表的指针。

在64位体系结构(现在很常见)中,一个指针是8字节。由于编译器实例化了大量的小对象,所以这些加起来非常快。

因此,人们正在努力尽可能多地删除虚函数(而且是实用的) ,并使用 switch指令实现虚函数,switch指令具有类似的执行速度,但对内存的影响要小得多。

他们对内存消耗的持续担忧得到了回报,例如,Clang 消耗的内存比 gcc 少得多,这在向客户提供库时非常重要。

另一方面,这也意味着添加一种新的节点通常会导致编辑大量文件中的代码,因为每个开关都需要进行调整(幸运的是,如果在开关中错过了一个枚举成员,编译器会发出警告)。因此,他们同意以提高记忆效率的名义增加维护的难度。

LLVM 推出自己的 RTTI 系统有几个原因。该系统简单、功能强大,在 LLVM 程序员手册的一个章节中进行了描述。正如另一张海报所指出的,编码标准提出了 C + + RTTI 的两个主要问题: 1)空间成本和2)使用它的性能差。

RTTI 的空间成本相当高: 每个具有 vtable (至少一个虚方法)的类都获得 RTTI 信息,其中包括类的名称和关于其基类的信息。此信息用于实现 打字机操作符以及 Dynamic _ cast。因为使用 vtable 支付每个类的成本(不,PGO 和链接时间优化没有帮助,因为 vtable 指向 RTTI 信息) LLVM 使用-fno-RTTI 构建。根据经验,这可以节省5-10% 的可执行文件大小,这是相当可观的。LLVM 不需要等效于 typeid,因此为每个类保留名称(type _ info 中的其他名称)只是浪费空间。

如果执行一些基准测试或查看为简单操作生成的代码,就很容易发现性能不佳。LLVM isa < > 操作符通常会编译成一个单独的加载并与常量进行比较(尽管类会根据它们如何实现 classof 方法来控制这一点)。下面是一个简单的例子:

#include "llvm/Constants.h"
using namespace llvm;
bool isConstantInt(Value *V) { return isa<ConstantInt>(V); }

汇总如下:

$ clang t.cc -S -o - -O3 -I$HOME/llvm/include -D__STDC_LIMIT_MACROS -D__STDC_CONSTANT_MACROS -mkernel -fomit-frame-pointer
...
__Z13isConstantIntPN4llvm5ValueE:
cmpb    $9, 8(%rdi)
sete    %al
movzbl  %al, %eax
ret

它(如果不读汇编)是一个加载并与常量进行比较。相比之下,Dynamic _ cast 的等价物是:

#include "llvm/Constants.h"
using namespace llvm;
bool isConstantInt(Value *V) { return dynamic_cast<ConstantInt*>(V) != 0; }

归纳起来就是:

clang t.cc -S -o - -O3 -I$HOME/llvm/include -D__STDC_LIMIT_MACROS -D__STDC_CONSTANT_MACROS -mkernel -fomit-frame-pointer
...
__Z13isConstantIntPN4llvm5ValueE:
pushq   %rax
xorb    %al, %al
testq   %rdi, %rdi
je  LBB0_2
xorl    %esi, %esi
movq    $-1, %rcx
xorl    %edx, %edx
callq   ___dynamic_cast
testq   %rax, %rax
setne   %al
LBB0_2:
movzbl  %al, %eax
popq    %rdx
ret

这需要更多的代码,但是对 _ _ Dynamic _ cast 的调用才是关键,它必须仔细研究 RTTI 数据结构,然后对这些内容进行一个非常通用的、动态计算的遍历。这比加载和比较要慢几个数量级。

好的,好的,所以它更慢,为什么这很重要?这很重要,因为 LLVM 做了大量的类型检查。优化器的许多部分都是围绕代码中的特定结构构建的,并对这些结构执行替换模式匹配。例如,下面是一些用于匹配简单模式的代码(这些代码已经知道 Op0/Op1是整数减法操作的左右边) :

  // (X*2) - X -> X
if (match(Op0, m_Mul(m_Specific(Op1), m_ConstantInt<2>())))
return Op1;

Match 操作符和 m _ * 是模板元程序,归结为一系列 isa/dyn _ cast 调用,每个调用都必须执行类型检查。对于这种细粒度的模式匹配来说,使用 Dynamic _ cast 会非常残酷,而且会非常缓慢。

最后,还有一点,那就是表现力。LLVM 使用的 不同的‘ rtti’运算符用于表示不同的事情: 类型检查、 Dynamic _ cast、强制(断言)强制转换、 null 处理等等。C + + 的 Dynamic _ cast 本身并不提供这些功能。

最后,有两种方式来看待这种情况。从消极的方面来看,C + + RTTI 对于很多人想要的东西(全反射)的定义过于狭窄,对于像 LLVM 这样简单的东西也过于缓慢。从积极的一面来看,C + + 语言足够强大,我们可以将这样的抽象定义为库代码,并选择不使用语言特性。关于 C + + ,我最喜欢的事情之一就是库的强大和优雅。RTTI 甚至不是我最不喜欢的 C + + 特性之一:) !

克里斯