C++ RTTI 有什么缺点,不适合使用?

74
阅读LLVM文档,他们提到他们使用一种自定义的RTTI形式,这就是为什么他们有isa<>cast<>dyn_cast<>模板函数的原因。
通常情况下,当你看到一个库重新实现了某种语言的基本功能时,这是一种糟糕的代码气味,只会引起运行问题。然而,我们正在谈论LLVM:这些家伙正在开发一个C++编译器和一个C++运行时。如果他们不知道他们在做什么,我就完蛋了,因为我更喜欢clang而不是Mac OS附带的gcc版本。
尽管如此,由于经验不足,我仍然想知道普通RTTI的缺陷。我知道它只适用于具有虚拟表的类型,但这只引出了两个问题:
  • 既然你只需要一个虚拟方法来拥有一个虚拟表,为什么他们不把一个方法标记为virtual呢?虚拟析构函数似乎很擅长这方面。
  • 如果他们的解决方案不使用常规RTTI,有什么想法是如何实现的吗?

3
当做出这些决定时,LLVM还不是C++编译器。他们还选择重新实现标准库功能,而且在相当长的时间内,他们的伪STL存在许多漏洞。 - Potatoswatter
@Potatoswatter 但是,现在他们已经制作了编译器,似乎仍然不打算重新考虑他们的选择。 - zneak
1
@Potatoswatter,你可能会对Chris Lattner(LLVM背后的人)在这里发布的答案感兴趣。 - zneak
有点奇怪,你竟然更喜欢 clang 而不是 gcc,考虑到它被别名为 clang 后端... https://dev59.com/wWIk5IYBdhLWcg3wJ7P4 - Cade Brown
@CadeBrown,早在2011年,Xcode 4就同时提供了gcc和Clang。 - zneak
4个回答

93
LLVM使用自己的RTTI系统有几个原因。该系统简单而强大,并在LLVM程序员手册的一节中进行了描述。正如另一个帖子所指出的,编码标准提出了C++ RTTI的两个主要问题:1)空间成本和2)使用它的性能不佳。
RTTI的空间成本相当高:每个具有虚函数表(至少一个虚方法)的类都会获得RTTI信息,其中包括类的名称和其基类的信息。此信息用于实现typeid运算符以及dynamic_cast。由于这个成本是为每个具有虚函数表的类支付的(不,PGO和链接时优化无法帮助,因为虚函数表指向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;

匹配运算符和m_*是模板元编程,其归结为一系列isa/dyn_cast调用,每个调用都需要进行类型检查。使用dynamic_cast进行这种细粒度的模式匹配将会非常缓慢。

最后,还有另一个点,那就是表现力。LLVM使用的不同的'rtti'运算符用于表示不同的内容:类型检查、dynamic_cast、强制(断言)转换、空处理等。C++的dynamic_cast没有(本地)提供任何这些功能。

最终,有两种看待这种情况的方式。消极的一面是,对于许多人想要的内容(完全反射),C++ RTTI定义过于狭窄,并且对于像LLVM这样简单的事情来说太慢了。积极的一面是,C++语言足够强大,我们可以将这样的抽象定义为库代码,并选择不使用语言特性。我最喜欢C++的一件事情就是它的库代码可以如此强大而优雅。RTTI甚至不在我最不喜欢的C++功能之列 :)!

-Chris


14
除非它们不是等效的操作。LLVM isa 不像 dynamic_cast 那样遵循继承关系。一个更好的比较是 if ( typeid(V) == typeid(ConstantInt *) ),GCC 将其映射到调用 strcmp 的函数。如果你想避免 strcmp,则可以假设编译器不会动态生成 typeinfo 对象,并使用 if ( &typeid(V) == &typeid(ConstantInt *) ),这在理论上是不可移植的,但这对你可能并不重要。 - Potatoswatter
4
LLVM内置的RTTI可以处理通过继承层次结构进行的直接向下转换,可以查看classof的示例实现(链接)。该实现是类型检查和转换操作的基础。之所以这种特定操作不行,是因为ConstantInt是一个叶子类,已经硬编码在它的classof实现中了。dynamic_cast从理论上讲也可以在链接时优化以处理叶子情况,但实际上并没有这样做。 - Stephen Lin

16

LLVM编码标准对此问题的回答似乎相当明确:

为了减少代码和可执行文件的大小,LLVM不使用RTTI(例如dynamic_cast<>)或异常。这两个语言特性违反了C++的“只付出你所使用的代价”的普遍原则,即使在代码库中从未使用过异常,或者从未为类使用过RTTI,它们也会导致可执行文件膨胀。因此,我们在代码中全局禁用它们。

尽管如此,LLVM广泛使用一种手工制作的RTTI形式,如isa<>、cast<>和dyn_cast<>。这种RTTI形式是可选择的,并且可以添加到任何类中。它比dynamic_cast<>要高效得多。


4
除此之外还是有些含糊其辞。链接器和PGO仍然可以找出那些未使用的内容,所以即使在某些情况下会产生影响,这真的有意义吗?如果它们被使用,但只是很少出现,那么你绝对可以兼得。 - Potatoswatter
1
@Potatoswatter: 这可能会很困难,因为LLVM和CLang是以多个库的形式分发的,并且你没有客户端程序... - Matthieu M.
2
如果客户端的二进制分布在许多可执行文件上,那么局部性就被抛弃了。我不知道在这种情况下代码膨胀有多相关。客户有责任在最终的单体二进制文件上应用 PGO。如果 LLVM 的组织方式阻止了这一点,那就是一个更大的问题。 - Potatoswatter

10

这里有一篇关于RTTI的好文章,讲述了为什么您可能需要自己编写版本。

虽然我不是C++ RTTI方面的专家,但我也实现了自己的RTTI,因为确实存在需要这样做的原因。首先,C++ RTTI系统的功能不太丰富,基本上只能进行类型转换和获取基本信息。如果您在运行时拥有一个类名字符串,并想构造该类的对象,那么使用C++ RTTI来完成这个操作就很棘手。此外,C++ RTTI实际上(或者说很难)无法跨模块(dll / so或exe)移植(您无法识别从另一个模块创建的对象的类)。同样,C++ RTTI的实现特定于编译器,并且通常在实现此功能的所有类型上打开它具有昂贵的额外开销。最后,它并不真正持久化,因此实际上不能用于文件保存/加载(例如,您可能希望将对象的数据保存到文件中,但也要保存其类的“typeid”,以便在加载时知道应该创建哪个对象以加载此数据,这不能可靠地使用C++ RTTI完成)。由于所有或某些原因,许多框架都有自己的RTTI(从非常简单的到非常丰富的)。例如wxWidget、LLVM、Boost.Serialization等。这真的很常见。

既然只需要一个虚方法就可以拥有vtable,为什么他们不将方法标记为虚拟的呢?虚析构函数似乎做得很好。

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

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

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


实际上,LLVM内部的RTTI并不使用虚函数表:基本上,每个继承层次结构的根类都有一个恒定的整数值,用于标识该层次结构中的每个子类。 - Stephen Lin
@StephenLin 很好知道。当然,有各种不同程度的自定义RTTI实现,这通常取决于您是否需要比标准RTTI更多(更重)的功能,或者您是否需要一个轻量级版本(即更少的功能)。是的,有手工制作/轻量级替代方案来避免依赖虚拟表,我猜LLVM团队感觉有必要使用这样的替代方案。但它仍然基本上是一种动态分派机制,通常通过虚函数来完成,但当然还有其他选择。 - Mikael Persson
当然,它基本上是一个基于标签的系统。也许他们应该使用机器学习来使生活更轻松 :D - Stephen Lin

4
主要原因是他们努力将内存使用保持在最低限度。
RTTI仅适用于至少包含一个虚方法的类,这意味着类的实例将包含指向虚表的指针。
在64位架构上(今天很常见),单个指针占用8个字节。由于编译器实例化了许多小对象,这会很快增加。
因此,正在不断努力尽可能地删除虚函数,并使用“switch”指令实现本应为虚函数的内容,它具有类似的执行速度,但内存影响显着降低。
他们对内存消耗的持续担忧已经得到了回报,例如Clang比gcc消耗的内存要少得多,这对于向客户提供库非常重要。
另一方面,这也意味着添加新类型的节点通常会导致编辑许多文件中的代码,因为每个switch都需要进行调整(幸运的是,如果您在switch中遗漏了枚举成员,编译器会发出警告)。因此,他们在内存效率的名义下接受了使维护稍微困难一些的问题。

那么,他们用什么替换了自己的RTTI中的8字节指针? - zneak
@zneak:枚举类型不太可能占用8个字节。大多数只占用一个字节。这意味着每个节点可以减少7个字节。典型的编译会分配几十万个节点。节省的空间以MB计算。虽然可能不多,但它们会累加。不过不用担心,他们使用massif等工具来减少关键内存的使用,并在一般情况下优先考虑速度而非内存。 - Matthieu M.
你确定枚举吗?看起来clang和gcc都会使sizeof(enum foo) == 4,即使只有一个元素的枚举(可能有一些属性可以设置大小为其他值)。另外,这是否意味着如果我想将定义在LLVM之外的类与他们的RTTI一起使用,我必须改变LLVM的源代码? - zneak
如果您有一个vtable,则RTTI结构将进入其中,每个类一个,而不是每个对象一个。 - Macke
@Macke:我从未说过其他的话,每个对象仍然有一个指针。@zneak:这是一种实现细节,我应该明确表明我所说的是最小值,唯一的保证是编译器将分配至少必要数量的位来表示所有值。在LLVM/CLang中,我似乎记得他们不存储枚举本身,而是使用位字段来存储其值,从而实现所需的压缩效果。 - Matthieu M.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接