什么可以使C ++ RTTI不受欢迎?

时间:2011-02-27 18:20:47

标签: c++ llvm rtti

查看LLVM文档,他们提到了they use "a custom form of RTTI",这就是他们拥有isa<>cast<>dyn_cast<>模板化函数的原因。

通常,阅读一个库重新实现一种语言的一些基本功能是一种可怕的代码味道,只是邀请运行。但是,这是我们所说的LLVM:这些人正在研究C ++编译器 C ++运行时。如果他们不知道他们在做什么,我会非常沮丧,因为我更喜欢clang到Mac OS附带的gcc版本。

尽管如此,由于缺乏经验,我不知道正常RTTI的缺陷是什么。我知道它只适用于有v-table的类型,但只提出两个问题:

  • 由于您只需要一个虚拟方法来获取vtable,为什么不将它们标记为virtual?虚拟析构者似乎对此很擅长。
  • 如果他们的解决方案不使用常规RTTI,任何想法如何实现?

4 个答案:

答案 0 :(得分:80)

LLVM推出自己的RTTI系统有几个原因。该系统简单而强大,并在the LLVM Programmer's Manual的一节中进行了描述。正如另一张海报所指出的那样,Coding Standards引发了C ++ RTTI的两个主要问题:1)空间成本和2)使用它的性能不佳。

RTTI的空间成本非常高:每个具有vtable(至少一个虚方法)的类都会获得RTTI信息,其中包括类的名称和有关其基类的信息。此信息用于实施typeid运算符以及dynamic_cast。因为使用vtable为每个类支付了这个成本(并且没有,PGO和链接时优化没有帮助,因为vtable指向RTTI信息)LLVM使用-fno-rtti构建。根据经验,这可以节省大约5-10%的可执行文件大小,这非常重要。 LLVM不需要等价的typeid,因此保留每个类的名称(以及type_info中的其他内容)只是浪费空间。

如果您进行基准测试或查看为简单操作生成的代码,则很容易看到性能不佳。 LLVM isa&lt;&gt;运算符通常编译为单个加载并与常量进行比较(尽管类根据它们如何实现其类方法来控制它)。这是一个简单的例子:

#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使用的different 'rtti' operators用于表达不同的东西:类型检查,动态广播,强制(断言)强制转换,空值处理等.C ++的dynamic_cast不(本机地)提供任何此功能。

最后,有两种方法可以看待这种情况。从消极方面来说,C ++ RTTI的定义过于狭窄,无法满足许多人的需求(全反射),并且对于像LLVM这样的简单事情来说太慢而无法使用。从积极的方面来说,C ++语言足够强大,我们可以将这样的抽象定义为库代码,并选择不使用语言功能。关于C ++我最喜欢的一件事是库是多么强大和优雅。在我最不喜欢的C ++ :)中,RTTI甚至不是很高!

-Chris

答案 1 :(得分:15)

LLVM coding standards似乎很好地回答了这个问题:

  

为了减少代码和可执行文件的大小,LLVM不使用RTTI(例如dynamic_cast&lt;&gt;)或异常。这两种语言特性违反了“你只需为所用内容付费”的一般C ++原则,即使从未在代码库中使用异常,或者RTTI从未用于类,也会导致可执行膨胀。因此,我们在代码中全局关闭它们。

     

也就是说,LLVM确实广泛使用了手动滚动形式的RTTI,它使用的模板如isa&lt;&gt;,cast&lt;&gt;和dyn_cast&lt;&gt;。这种形式的RTTI是可选的,可以添加到任何类中。它也比dynamic_cast&lt;&gt;。

更有效

答案 2 :(得分:10)

Here是一篇关于RTTI的精彩文章,以及为什么你可能需要推出自己的版本。

我不是C ++ RTTI的专家,但我也实现了自己的RTTI,因为肯定有理由说明你需要这样做。首先,C ++ RTTI系统功能不是很丰富,基本上你所能做的就是输入和获取基本信息。如果在运行时,你有一个带有类名的字符串,并且你想要构造该类的对象,那么用C ++ RTTI做这件事就好了。此外,C ++ RTTI并不是真正(或容易)跨模块移植(您无法识别从另一个模块(dll / so或exe)创建的对象的类。同样,C ++ RTTI的实现特定于编译器,并且在为所有类型实现这一点的额外开销方面开启通常是昂贵的。最后,它并不是真正持久的,因此它不能真正用于文件保存/加载(例如,您可能想要保存一个对象到文件的数据,但你也想保存它的类的“typeid”,这样,在加载时,你知道要创建哪个对象来加载这个数据,用C ++无法可靠地完成RTTI)。出于所有或部分原因,许多框架都有自己的RTTI(从非常简单到功能非常丰富)。例如wxWidget,LLVM,Boost.Serialization等。这真的不常见。< / p>

  
    

由于你只需要一个虚拟方法来获得vtable,为什么它们不只是将方法标记为虚拟?虚拟析构者似乎对此很擅长。

  

这可能是他们的RTTI系统也使用的。虚函数是动态绑定(运行时绑定)的基础,因此,它基本上是执行任何类型的运行时类型标识/信息所必需的(不仅仅是C ++ RTTI要求,但任何RTTI实现都会有以这种或那种方式依赖虚拟呼叫。

  
    

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

  

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

答案 3 :(得分:4)

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

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

在64位架构(今天很常见)上,单个指针是8个字节。由于编译器实例化了许多小对象,因此很快就会增加。

因此,一直在努力尽可能地(和实际地)删除虚函数,并使用switch指令实现虚函数,该指令具有类似的执行速度但内存影响显着降低

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

另一方面,这也意味着添加一种新节点通常会导致编辑大量文件中的代码,因为每个开关都需要进行调整(谢天谢地,如果你错过了交换机中的枚举成员,编译器会发出警告)。因此,他们接受以记忆效率的名义使维护更加困难。