什么会使C ++ RTTI不受欢迎?


67

在查看LLVM文档时,他们提到他们使用“ RTTI的自定义形式”,这就是他们拥有isa<>cast<>dyn_cast<>模板功能。

通常,读到库重新实现某种语言的某些基本功能后,这会产生可怕的代码味道,并且会被邀请运行。但是,这就是我们所说的LLVM:这些家伙正在研究C ++编译器C ++运行时。如果他们不知道自己在做什么,那我就很困惑,因为我更喜欢Mac OS附带clanggcc版本。

尽管如此,由于经验不足,我仍然想知道普通RTTI的陷阱是什么。我知道它仅适用于具有v表的类型,但这仅引发两个问题:

  • 由于您只需要一个虚拟方法来拥有一个vtable,为什么他们不只是将一个方法标记为virtual?虚拟析构函数似乎擅长于此。
  • 如果他们的解决方案不使用常规的RTTI,是否知道如何实现?

3
在做出这些决定时,LLVM并不是C ++编译器。他/他们还选择了重新实现标准库的功能,并且很长一段时间以来,他们的伪STL都是错误的。
Potatoswatter

@Potatoswatter尽管如此,既然他们确实制作了编译器,他们似乎并没有重新考虑他们的选择。
zneak

1
@Potatoswatter您可能对Chris Lattner(LLVM背后的家伙)在这里发布的答案感兴趣。
zneak 2011年

一种奇怪的,你喜欢clanggcc,考虑到它的别名到铛后端... stackoverflow.com/questions/19535422/...
凯德·布朗

@CadeBrown,早在2011年,Xcode 4就同时交付了gcc和Clang。
zneak

Answers:


84

LLVM推出自己的RTTI系统的原因有很多。该系统简单而强大,并且在《 LLVM程序员手册》的一部分中进行介绍。正如另一位海报指出的那样,编码标准在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 <>运算符通常编译为单个负载并与常量进行比较(尽管类是基于它们实现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)
    设定%al
    movzbl%al,%eax
    退回

(如果您不阅读汇编的话)是负载,并与常量进行比较。相反,dynamic_cast的等效项是:

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

编译成:

铛t.cc -S -o--O3 -I $ HOME / llvm / include -D__STDC_LIMIT_MACROS -D__STDC_CONSTANT_MACROS-内核-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
    塞特纳尔
LBB0_2:
    movzbl%al,%eax
    popq%rdx
    退回

这是很多代码,但杀手是对__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,强制(声明)强制转换,空处理等。C++的dynamic_cast不(本机)提供任何此功能。

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

-克里斯


13
除了那些不是等效的操作。LLVMisa不像那样尊重继承dynamic_cast。更好的比较是if ( typeid(V) == typeid(ConstantInt *) ),该GCC映射到一个函数,我将假设该函数调用strcmp的是整形的类型名。如果要避免使用strcmp,则可以假定编译器不会动态生成typeinfo对象并使用if ( &typeid(V) == &typeid(ConstantInt *) ),从理论上讲这是不可移植的,但这对您而言并不重要。
Potatoswatter

4
@Potatoswatter的回复非常过时,但是要澄清一下,以防万一有人遇到这种情况,LLVM的内置RTTI将可以通过继承层次结构处理直接向下转换,请参阅classof (link)的示例实现,这是类型检查和强制转换操作的基础。该特定操作之所以没有,是因为ConstantInt是一个叶子类,该类被硬编码到其实现中classofdynamic_cast可以在理论上进行优化以在链接时也处理叶子情况,但实际上没有。

15

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.11年

2
@Matthieu:如果客户端的二进制分发分布在许多可执行文件上,则局部性是注销。我不知道这种情况下相关的代码膨胀如何。将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等。这确实并不罕见。许多框架都有自己的RTTI(从非常简单到功能非常丰富)。例子是wxWidget,LLVM,Boost.Serialization等。这确实并不罕见。许多框架都有自己的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

当然,它基本上是基于标签的系统。也许他们应该只是改用ML,让他们的生活更轻松:D
Stephen Lin

4

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

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

在64位体系结构(今天很常见)上,单个指针为8个字节。由于编译器实例化了许多小对象,因此加起来很快。

因此,人们正在不断努力,以尽可能地(且切实可行的)去除虚拟功能,并利用虚拟机来实现虚拟功能。 switch指令指令,该指令具有类似的执行速度,但对内存的影响明显较低。

他们对内存消耗的持续担忧已经得到了回报,例如,Clang消耗的内存明显少于gcc,这在将库提供给客户端时很重要。

另一方面,这也意味着添加新种类的节点通常会导致在大量文件中编辑代码,因为需要调整每个开关(如果您错过了开关中的枚举成员,则编译器会发出警告)。因此,他们接受了以内存效率的名义使维护工作变得更加困难。


那么,他们将自己的RTTI替换为8字节指针是什么呢?
zneak 2011年

@zneak:一个枚举不太可能占用8个字节。大多数只占用一个字节。每个节点少7字节。典型的编译分配了数十万个节点。节省量以MB为单位。可能不多,但是加起来。不过,请放心,他们使用诸如Massif之类的工具来减少需要计数的内存,并通常获得对内存的特权速度。
Matthieu M.

您确定枚举吗?似乎clang和gcc make都可以sizeof(enum foo) == 4,即使枚举仅包含一个元素(尽管可能会有一些属性将大小设置为其他值)。另外,这是否意味着如果我想将其RTTI与LLVM之外定义的类一起使用,是否必须更改LLVM的源代码?
zneak 2011年

如果您有一个vtable,则RTTI结构进入其中,每个类一个,而不是每个对象一个。
Macke

@Macke:我从来没有说过,每个对象仍然有一个指针。@zneak:这是一个实现细节,我应该明确地说起我所说的最小值,唯一的保证是编译器将分配至少必要数量的位来表示所有值。在LLVM / CLang中,我似乎想起了它们并不存储枚举本身,而是使用位字段存储其值,从而获得所需的压缩效果。
Matthieu M.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.