什么是typeid的成本?

Dav*_*vid 10 c++ type-erasure c++11

我正在考虑使用typeid来解析类型的类型擦除设置......

struct BaseThing
{
    virtual ~BaseThing() = 0 {}
};

template<typename T>
struct Thing : public BaseThing
{
    T x;
};

struct A{};
struct B{};

int main() 
{
    BaseThing* pThing = new Thing<B>();
    const std::type_info& x = typeid(*pThing);

    if( x == typeid(Thing<B>))
    {
        std::cout << "pThing is a Thing<B>!\n";
        Thing<B>* pB = static_cast<Thing<B>*>(pThing);
    }
    else if( x == typeid(Thing<A>))
    {
        std::cout << "pThing is a Thing<A>!\n";
        Thing<A>* pA = static_cast<Thing<A>*>(pThing);
    }
}
Run Code Online (Sandbox Code Playgroud)

我从未见过其他人这样做过.替代方案是BaseThing具有纯虚拟GetID(),用于推断类型而不是使用typeid.在这种情况下,只有1级继承,typeid的成本与虚函数调用的成本是多少?我知道typeid以某种方式使用vtable,但它究竟是如何工作的呢?

这是可取的而不是GetID(),因为尝试确保ID是唯一确定的,需要相当多的hackery .

Quu*_*one 14

替代方案是BaseThing具有纯虚拟GetID(),用于推断类型而不是使用typeid.在这种情况下,只有1级继承,typeid的成本与虚函数调用的成本是多少?我知道typeid以某种方式使用vtable,但它究竟是如何工作的呢?

在Linux和Mac上,或使用Itanium C++ ABI的任何其他内容,typeid(x)编译成两个加载指令 - 它只是从对象的前8个字节加载vptr(即某些vtable的地址)x,然后-1从中加载th指针那个vtable.那个指针是&typeid(x).这是一个比调用虚方法便宜的函数调用.

在Windows上,它涉及四个加载指令和几个(可忽略的)ALU操作的顺序,因为Microsoft C++ ABI 更加企业化.(来源)老实说,这可能最终与虚拟方法调用相提并论.但与a相比,这仍然便宜dynamic_cast.

A dynamic_cast涉及到C++运行时的函数调用,它有很多负载和条件分支等.

所以,是的,开发typeid将是非常非常快于dynamic_cast.这将是正确的为您的使用情况- ?这是值得商榷的.(参见关于Liskov可替代性等的其他答案.)但它会快吗? - 是的

在这里,我从Vaughn高度评价的答案中获取了玩具基准代码,并将其作为一个实际的基准,避免了明显的循环提升优化,这些都优化了他的所有时间.结果,对于我的Macbook上的libc ++ abi:

$ g++ test.cc -lbenchmark -std=c++14; ./a.out
Run on (4 X 2400 MHz CPU s)
2017-06-27 20:44:12
Benchmark                   Time           CPU Iterations
---------------------------------------------------------
bench_dynamic_cast      70407 ns      70355 ns       9712
bench_typeid            31205 ns      31185 ns      21877
bench_id_method         30453 ns      29956 ns      25039

$ g++ test.cc -lbenchmark -std=c++14 -O3; ./a.out
Run on (4 X 2400 MHz CPU s)
2017-06-27 20:44:27
Benchmark                   Time           CPU Iterations
---------------------------------------------------------
bench_dynamic_cast      57613 ns      57591 ns      11441
bench_typeid            12930 ns      12844 ns      56370
bench_id_method         20942 ns      20585 ns      33965
Run Code Online (Sandbox Code Playgroud)

(ns越低越好.你可以忽略后两列:"CPU"只是表明它正在花费所有时间运行而没有时间等待,而"迭代"只是为了获得良好的误差而花费的运行次数. )

您甚至可以看到typeidthrashes ,但是当您启用优化时,它甚至会更好 - 因为编译器可以优化编写的任何代码.隐藏在libc ++ abi 函数中的所有丑陋代码都不能被编译器优化,因此开启时没有多大帮助.dynamic_cast-O0__dynamic_cast-O3


Vau*_*ato 7

通常,您不仅要知道类型,还要对该类型的对象执行某些操作.在这种情况下,dynamic_cast更有用:

int main() 
{
    BaseThing* pThing = new Thing<B>();

    if(Thing<B>* pThingB = dynamic_cast<Thing<B>*>(pThing)) {
    {
        // Do something with pThingB
    }
    else if(Thing<A>* pThingA = dynamic_cast<Thing<A>*>(pThing)) {
    {
        // Do something with pThingA
    }
}
Run Code Online (Sandbox Code Playgroud)

我想这就是为什么你很少看到在实践中使用的typeid.

更新:

由于这个问题与性能有关.我在g ++ 4.5.1上运行了一些基准测试.使用此代码:

struct Base {
  virtual ~Base() { }
  virtual int id() const = 0;
};

template <class T> struct Id;

template<> struct Id<int> { static const int value = 1; };
template<> struct Id<float> { static const int value = 2; };
template<> struct Id<char> { static const int value = 3; };
template<> struct Id<unsigned long> { static const int value = 4; };

template <class T>
struct Derived : Base {
  virtual int id() const { return Id<T>::value; }
};

static const int count = 100000000;

static int test1(Base *bp)
{
  int total = 0;
  for (int iter=0; iter!=count; ++iter) {
    if (Derived<int>* dp = dynamic_cast<Derived<int>*>(bp)) {
      total += 5;
    }
    else if (Derived<float> *dp = dynamic_cast<Derived<float>*>(bp)) {
      total += 7;
    }
    else if (Derived<char> *dp = dynamic_cast<Derived<char>*>(bp)) {
      total += 2;
    }
    else if (
      Derived<unsigned long> *dp = dynamic_cast<Derived<unsigned long>*>(bp)
    ) {
      total += 9;
    }
  }
  return total;
}

static int test2(Base *bp)
{
  int total = 0;
  for (int iter=0; iter!=count; ++iter) {
    const std::type_info& type = typeid(*bp);

    if (type==typeid(Derived<int>)) {
      total += 5;
    }
    else if (type==typeid(Derived<float>)) {
      total += 7;
    }
    else if (type==typeid(Derived<char>)) {
      total += 2;
    }
    else if (type==typeid(Derived<unsigned long>)) {
      total += 9;
    }
  }
  return total;
}

static int test3(Base *bp)
{
  int total = 0;
  for (int iter=0; iter!=count; ++iter) {
    int id = bp->id();
    switch (id) {
      case 1: total += 5; break;
      case 2: total += 7; break;
      case 3: total += 2; break;
      case 4: total += 9; break;
    }
  }
  return total;
}
Run Code Online (Sandbox Code Playgroud)

没有优化,我得到了这些运行时:

test1: 2.277s
test2: 0.629s
test3: 0.469s
Run Code Online (Sandbox Code Playgroud)

通过优化-O2,我得到了这些运行时:

test1: 0.118s
test2: 0.220s
test3: 0.290s
Run Code Online (Sandbox Code Playgroud)

因此,当使用此编译器进行优化时,dynamic_cast似乎是最快的方法.

  • 我在答案中发布了一个真正的基准.Vaughn基准测试的问题在于编译器在识别和提升循环不变量时非常好*.在循环中反复测试相同的条件与在循环顶部测试一次没有什么不同.如果编译器看到你在地板上丢弃`result`,那就更糟了 - 在这种情况下根本不需要编译函数! (4认同)
  • 看了一下生成的代码.似乎test2甚至没有生成循环,结果是在编译时知道的. (3认同)
  • 这很有趣,因为我尝试用g ++ - 4.7运行那些睾丸,结果与-O2完全不同9.84s,0.16s,0.31.它在-O3中更加清晰:5.71s,0.002s,0.311s.事实上似乎相当公平,因为编译器可以优化测试2和3中的"开关",同时它必须执行test1中的每个演员.在编译时,编译器肯定会在test2中摆脱多态,然后在编译时知道typeid的结果. (2认同)