【问题标题】:What can make C++ RTTI undesirable to use?什么会使 C++ RTTI 不受欢迎?
【发布时间】:2011-07-05 07:59:40
【问题描述】:

查看 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,知道它是如何实现的吗?

【问题讨论】:

  • LLVM 在做出这些决定时还不是 C++ 编译器。他/他们还选择重新实现标准库功能,很长一段时间以来,他们的伪 STL 都存在相当大的缺陷。
  • @Potatoswatter 尽管如此,他们似乎并没有重新考虑他们的选择,因为他们确实制作了一个编译器。
  • @Potatoswatter 您可能对 Chris Lattner(LLVM 背后的人)在此处发布的答案感兴趣。
  • 有点奇怪,你更喜欢 clanggcc,考虑到它是 clang 后端的别名...stackoverflow.com/questions/19535422/…
  • @CadeBrown,早在 2011 年,Xcode 4 就同时发布了 gcc 和 Clang。

标签: c++ llvm rtti


【解决方案1】:

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 运算符通常编译为单个加载并与常量进行比较(尽管类根据它们实现其 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 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 设置%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 用于表达不同的东西:类型检查、dynamic_cast、强制(断言)强制转换、空处理等。C++ 的 dynamic_cast(本机)不提供任何这些功能。

最后,有两种方法来看待这种情况。不利的一面是,C++ RTTI 对许多人想要的东西(完全反射)的定义过于狭隘,而且对于像 LLVM 所做的那样简单的事情也太慢了。从积极的方面来说,C++ 语言足够强大,我们可以将这样的抽象定义为库代码,并选择不使用语言特性。关于 C++,我最喜欢的一件事是库的强大和优雅。在我最不喜欢的 C++ 特性中,RTTI 甚至不是很高 :)!

-克里斯

【讨论】:

  • 除了那些不是等效的操作。 LLVM isa 不像 dynamic_cast 那样尊重继承。一个更好的比较是if ( typeid(V) == typeid(ConstantInt *) ),GCC 映射到一个函数,我假设在损坏的类型名上调用strcmp。如果你想避免strcmp,那么你可以假设编译器不会动态生成typeinfo对象并使用if ( &amp;typeid(V) == &amp;typeid(ConstantInt *) ),这在理论上是不可移植的,但这对你来说并不重要。
  • @Potatoswatter 非常过时的回复,但为了澄清以防有人遇到此问题,LLVM 的内置 RTTI 将可以通过继承层次结构处理直接向下转换,请参阅 classof (link) 的示例实现,这是类型检查和强制转换操作的基础......这个特定操作没有的原因是因为 ConstantInt 是一个叶类,它被硬编码到其classof 的实现中。 dynamic_cast 理论上也可以在链接时进行优化以处理叶子案例,但实际上并没有。
【解决方案2】:

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

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

也就是说,LLVM 确实广泛使用了手动形式的 RTTI,它使用了诸如 isa、cast 和 dyn_cast 之类的模板。这种形式的 RTTI 是可选的,可以添加到任何课程中。它也比 dynamic_cast 更有效。

【讨论】:

  • 除非那仍然是手波。链接器和PGO仍然可以弄清楚那些东西没有被使用,那么即使在某些情况下有影响,它真的有意义吗?如果它们被使用,但很少使用,那么您肯定会得到两全其美的效果。
  • @Potatoswatter:这会很困难,因为 LLVM 和 CLang 是作为大量库分发的,而您没有客户端程序...
  • @Matthieu:如果客户端的二进制分发分布在许多可执行文件上,那么局部性就是一个注销。在这种情况下,我不知道代码膨胀有多相关。在最终的单体二进制文件上应用 PGO 是客户的责任。如果 LLVM 的组织以某种方式阻止了这一点,那就是一个更大的问题。
【解决方案3】:

Here 是一篇关于 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 不使用虚拟表:基本上每个继承层次结构的每个根类都有一个常量整数值,用于标识该层次结构中的每个子类。
  • @StephenLin 很高兴知道。当然有不同程度的自定义 RTTI 实现,这通常取决于您是否需要比标准 RTTI 更多(更重)的功能,或者您是否需要更轻量级的版本(即更多最少的功能)。是的,有依赖虚拟表的手动/轻量级替代方案,我猜 LLVM 团队觉得有必要使用这种替代方案。但它本质上仍然是一种动态调度机制,通常通过虚函数来完成,但当然也有替代方案。
  • 当然,它基本上是一个基于标签的系统。也许他们应该改用 ML,让他们的生活更轻松:D
【解决方案4】:

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

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

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

因此,我们一直在努力尽可能多地(实用地)移除虚函数,并使用 switch 指令实现本来应该是虚函数的东西,该指令具有相似的执行速度但显着降低了内存影响。

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

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

【讨论】:

  • 那么他们为自己的 RTTI 用什么替换了 8 字节指针?
  • @zneak:枚举不太可能占用 8 个字节。大多数只占用一个字节。每个节点少 7 个字节。典型的编译会分配数十万个节点。节省以 MB 为单位。它可能不多,但它加起来。不过不用担心,他们使用诸如 massif 之类的工具来减少重要的内存,并在一般情况下提高内存速度。
  • 你确定枚举吗?似乎 clang 和 gcc 都生成了sizeof(enum foo) == 4,即使只有一个元素的枚举(尽管可能有一些属性可以将大小设置为其他值)。另外,这是否意味着如果我想将他们的 RTTI 与我在 LLVM 之外定义的类一起使用,我必须更改 LLVM 的源代码?
  • 如果你有一个 vtable,RTTI 结构就会进入其中,每个类一个,而不是每个对象一个。
  • @Macke:我从来没有说过,每个对象仍然有一个指针。 @zneak:这是一个实现细节,我应该说清楚我说的是最小值,唯一的保证是编译器将分配至少必要的位数来表示所有值。在 LLVM/CLang 中,我似乎记得他们不存储枚举本身,而是使用位域来存储它的值,从而达到所需的压缩效果。
猜你喜欢
  • 2016-01-20
  • 1970-01-01
  • 2021-04-04
  • 2010-10-20
  • 2011-06-15
  • 2015-05-23
  • 2019-06-11
  • 2017-05-30
  • 2017-08-04
相关资源
最近更新 更多