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

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

通常,阅读一个库重新实现一些语言的一些基本function是一个可怕的代码气味,只是邀请运行。 但是,我们正在谈论的是LLVM:这些人正在研究C ++编译器 C ++运行时。 如果他们不知道自己在做什么,那么我就非常烦了,因为我更喜欢使用Mac OS附带的gcc版本。

尽pipe如此,还是比他们less尝试,我仍然想知道正常RTTI的缺陷是什么。 我知道它只适用于具有V表的types,但是这只会引出两个问题:

  • 既然你只是需要一个虚拟的方法来有一个虚拟表,为什么他们不只是一个方法标记为virtual ? 虚拟析构函数似乎擅长这一点。
  • 如果他们的解决scheme不使用普通的RTTI,那么有什么想法是如何实现的?

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

RTTI的空间成本相当高:每个带有vtable的类(至less有一个虚拟方法)获取RTTI信息,其中包括类的名称和基类的信息。 这些信息用于实现typeid操作符以及dynamic_cast 。 因为这个成本是用vtable为每个类支付的(并且不,PGO和链接时间优化没有帮助,因为vtable指向RTTI信息)LLVM使用-fno-rtti构build。 从经验上讲,这节省了5-10%的可执行文件的大小,这是非常可观的。 LLVM不需要相当于typeid,所以每个类的名字(在type_info中)都是浪费空间。

如果您执行一些基准testing或查看为简单操作生成的代码,性能差就很容易看出来。 LLVM isa <>操作符通常编译为一个单一的负载,并与一个常量进行比较(尽pipe类根据它们如何实现它们的类方法来控制它)。 这是一个微不足道的例子:

 #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数据结构,并做一个非常普遍的,dynamic计算通过这个东西走。 这是比负载慢几个数量级的比较。

好吧,好吧,这样慢一点,为什么这很重要? 这很重要,因为LLVM做了很多types的检查。 优化器的许多部分是围绕模式匹配代码中的特定构造,并对其进行replace而构build的。 例如,下面是一些用于匹配简单模式的代码(它已经知道Op0 / Op1是整数相减操作的左侧和右侧):

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

匹配运算符和m_ *是模板元程序,归结为一系列isa / dyn_cast调用,每个调用都必须进行types检查。 使用dynamic_cast进行这种细粒度的模式匹配将是残酷的,而且显得很慢。

最后还有一点,就是expression性。 LLVM使用的不同“rtti”运算符用于表示不同的东西:types检查,dynamic_cast,强制(断言)转换,空处理等.C ++的dynamic_cast不会(本地)提供任何这种function。

最后,有两种方法来看待这种情况。 从负面的angular度来看,C ++ RTTI对于许多人所需要的(完全反思)都是过于狭义的,对于像LLVM那样简单的事情来说,它太慢而不能用。 从积极的方面来说,C ++语言是非常强大的,我们可以像这样定义抽象类库代码,并select不使用语言特性。 我最喜欢的关于C ++的东西之一就是库的强大和优雅。 在我最不喜欢的C ++特性中,RTTI并不是很高!

-克里斯

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

为了减less代码和可执行文件的大小,LLVM不使用RTTI(例如dynamic_cast <>)或exception。 这两种语言特性违反了“你只付出你所用的东西”的一般C ++原则,即使在代码库中从不使用exception,或者RTTI从不用于类,也会导致可执行文件膨胀。 因此,我们在代码中全局closures它们。

也就是说,LLVM确实广泛使用了使用像isa <>,cast <>和dyn_cast <>这样的模板的RTTI手动滚动forms。 这种forms的RTTI是可选的,可以添加到任何课程。 它也比dynamic_cast <>效率更高。

这里有一篇关于RTTI的文章,为什么你可能需要推出自己的版本。

我不是C ++ RTTI方面的专家,但是我也实现了自己的RTTI,因为肯定有这样的理由。 首先,C ++ RTTI系统function不是很丰富,基本上所有你能做的就是进行types转换和获取基本信息。 如果在运行时你有一个带有类名的string,并且你想要构造该类的一个对象,那么使用C ++ RTTI来做这件事情会很好。 另外,C ++ RTTI并不是真正(或者很容易)跨模块的移植(你不能识别从另外一个模块(dll / so或者exe)创build的对象的类,同样,C ++ RTTI的实现也是特定于编译器的,在开销方面,开销通常是昂贵的,对于所有types实现这个额外的开销,最后,它并不是真正的持久性,所以它不能真正用于文件保存/加载(例如,你可能想要保存一个对象的数据到一个文件,但是你也想保存它的类的“typeid”,这样在加载的时候,你知道为了加载这个数据而创build的对象,这不能用C ++可靠地完成RTTI)由于所有或部分原因,许多框架都有自己的RTTI(从简单到function丰富),例如wxWidget,LLVM,Boost.Serialization等等,这实际上并不罕见。

既然你只是需要一个虚拟的方法来有一个虚拟表,为什么他们不标记一个方法为虚拟? 虚拟析构函数似乎擅长这一点。

这可能是他们的RTTI系统所使用的。 虚函数是dynamic绑定(运行时绑定)的基础,因此,它基本上是做任何types的运行时types识别/信息(不仅仅是C ++ RTTI所要求的),但是RTTI的任何实现都将具有以这种或那种方式依靠虚拟呼叫)。

如果他们的解决scheme不使用普通的RTTI,那么有什么想法是如何实现的?

当然,你可以在C ++中查找RTTI实现。 我已经做了我自己的,有很多图书馆也有自己的RTTI。 真的写起来相当简单。 基本上,所有你需要的是一种手段来唯一地表示一个types(即类的名称,或它的一些损坏的版本,甚至每个类的唯一的ID),某种结构类似于type_info包含所有的信息关于你需要的types,那么你需要在每个类中有一个“隐藏的”虚函数,这个虚函数会根据请求返回这个types的信息(如果这个函数在每个派生类中被覆盖,它将会工作)。 当然,还有一些额外的东西可以完成,比如所有types的单一存储库,也许还有相关的工厂函数(当运行时所有已知的名称都可以用来创build一个types的对象时,这是非常有用的的types,作为string或typesID)。 此外,您可能希望添加一些虚拟函数来允许dynamictypes转换(通常这是通过调用最大派生类的转换函数并执行static_cast到您希望转换的types来完成的)。

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

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

在64位架构(今天很常见)上,一个指针是8个字节。 由于编译器实例化大量的小对象,这相加很快。

因此,为了尽可能(和实用)地去除虚拟function并且实施具有类似执行速度但是显着降低内存影响的switch指令的虚拟function,正在进行努力。

他们对内存消耗的不断担心已经得到了回报,例如,Clang的内存消耗比gccless得多,这对于向客户提供库时非常重要。

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