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

zne*_*eak 67 c++ rtti llvm

纵观LLVM文件,他们提到,他们使用"RTTI的自定义窗体",这是他们的原因isa<>,cast<>dyn_cast<>模板功能.

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

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

  • 既然你只需要一个虚拟方法来获得vtable,为什么它们不只是将方法标记为virtual?虚拟析构函数似乎很擅长这一点.
  • 如果他们的解决方案不使用常规RTTI,任何想法如何实现?

小智 81

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 <>运算符通常编译为单个加载并与常量进行比较(尽管类根据它们实现类方法的方式来控制它).这是一个简单的例子:

#include "llvm/Constants.h"
using namespace llvm;
bool isConstantInt(Value *V) { return isa<ConstantInt>(V); }
Run Code Online (Sandbox Code Playgroud)

这编译为:

$ 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; }
Run Code Online (Sandbox Code Playgroud)

编译为:

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;
Run Code Online (Sandbox Code Playgroud)

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

最后,还有另一点,即表现力.LLVM使用的不同'rtti'运算符用于表达不同的东西:类型检查,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(&typeid(V)==&typeid(ConstantInt*))`,这在理论上是不可移植的但是对你不重要. (13认同)
  • @Potatoswatter非常过时的回复,但为了澄清以防有人遇到这个问题,LLVM的内置RTTI可以通过继承层次处理直接向下转换,请参阅`classof` [(link)]的示例实现(http://llvm.org/ docs/HowToSetUpLLVMStyleRTTI.html)这是类型检查和转换操作的基础......这个特定操作不是因为ConstantInt是一个叶子类,它被硬编码到`classof`的实现中.理论上,`dynamic_cast`可以在链接时进行优化以处理叶子情况,但实际上并没有. (3认同)

Jer*_*fin 15

LLVM编码标准似乎回答这个问题相当好:

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

也就是说,LLVM确实广泛使用了手动形式的RTTI,它使用了isa <>,cast <>和dyn_cast <>等模板.这种形式的RTTI是可选的,可以添加到任何类中.它也比dynamic_cast <>更有效.

  • 除了那仍然是手工波浪.链接器和PGO仍然可以发现那些东西没有被使用,所以即使在某些情况下有影响,它真的有意义吗?如果他们被使用,但很少,你绝对可以充分利用这两个世界. (4认同)
  • @Matthieu:如果客户端的二进制分布分布在许多可执行文件上,那么locality就是注销.在这种情况下,我不知道代码膨胀是多么相关.客户有责任在最终的单片二进制文件上应用PGO.如果LLVM的组织以某种方式阻止了这一点,那就是一个更大的问题. (2认同)

Mik*_*son 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您希望转换为的类型来完成).