【问题标题】:What's the cost of typeid?typeid的成本是多少?
【发布时间】:2011-11-11 13:43:52
【问题描述】:

我正在考虑使用 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);
    }
}

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

这将是可取的,而不是 GetID(),因为要确保 ID 是唯一的确定性需要相当多的技巧。

【问题讨论】:

  • 最初,虚拟继承旨在避免您在 main 函数中使用的那种 switch 或 if-else-chain。如果有人忘记添加 else,这个开关很容易出错。您是否完全确定虚拟分派或双重分派不会在您的情况下完成这项工作?
  • Boost.Any 使用typeid(与static_cast 结合使用)好吧。用户代码使用get 函数,并且不打扰typeid。 - 实际上,您最好使用它,而不是自己滚动,因为它为您做的更多(克隆和管理动态内存)。
  • @thiton 考虑一下当今世界上的大多数事件处理程序方法 - 它们几乎都具有与上述等效的 switch 或 if/else 链。
  • @visitor Boost.Any 看起来很完美,除了它没有针对右值引用进行更新,它可以从 make_any 中受益,就像 shared_ptr 有一个 make_shared 一样,并且 any_cast 不能正确替换 dynamic_cast(见下面 celtschk 的回答为什么)。
  • @Dave:我不明白 dynamic_cast 的意思。 Boost.Any 中没有中间类型。

标签: c++ c++11 type-erasure


【解决方案1】:

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

在 Linux 和 Mac 或任何其他使用 Itanium C++ ABI 的平台上,typeid(x) 编译成两条加载指令——它只是从对象 @987654327 的前 8 个字节加载 vptr(即某个 vtable 的地址) @,然后从该 vtable 加载 -1th 指针。该指针是&amp;typeid(x)。这是一个函数调用比调用虚方法更便宜

在 Windows 上,它涉及大约 四个 加载指令和几个(可忽略不计的)ALU 操作,因为 Microsoft C++ ABI 是 a bit more enterprisey。 (source) 老实说,这最终可能与虚拟方法调用相当。但与dynamic_cast 相比,这仍然非常便宜。

dynamic_cast 涉及对 C++ 运行时的函数调用,它具有很多 加载和条件分支等。

所以是的,利用typeid 将比dynamic_cast很多。对于您的用例,它是否正确?——这是值得怀疑的。 (请参阅有关 Liskov 可替代性等的其他答案。)但是它会 快吗? - 是的。

在这里,我从 Vaughn 的高评价答案中提取了玩具基准代码,并将其放入 an actual benchmark,避免了明显的循环提升优化,这会影响他的所有时间。结果,对于我的 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

(较低的ns 更好。您可以忽略后两列:“CPU”只是表示它正在运行所有时间并且没有时间等待,“Iterations”只是它获得的运行次数误差很大。)

您可以看到typeid 甚至在-O0 上也超过dynamic_cast,但是当您打开优化时,效果会更好——因为编译器可以优化 编写的任何代码。 All that ugly code hidden inside libc++abi's __dynamic_cast function 无法被编译器优化,因此打开 -O3 并没有多大帮助。

【讨论】:

  • 谢谢!很高兴看到这一点。我想知道为什么动态演员在 Vaughn 的回答中出现得更快。
【解决方案2】:

通常,您不仅想知道类型,还想对作为该类型的对象做一些事情。在这种情况下,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
    }
}

我认为这就是为什么您在实践中很少看到 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;
}

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

test1: 2.277s
test2: 0.629s
test3: 0.469s

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

test1: 0.118s
test2: 0.220s
test3: 0.290s

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

【讨论】:

  • 如果我知道类型,我就不需要使用动态转换 - 所以首先如果我愿意:Thing&lt;B&gt;* pB = static_cast&lt;Thing&lt;B&gt;*&gt;(pThing);。使用 typeid 和 static_cast 会比 n 个 dynamic_cast 快很多。
  • @Dave 为什么typeid 应该比dynamic_cast 快?当然,在您的情况下,您知道类型(因为typeid),但是一旦dynamic_cast 返回非0(或不抛出),您也知道类型并且已经得到了您的演员。
  • 这很有趣,因为我尝试使用 g++-4.7 运行这些睾丸,而使用 -O2 的结果完全不同,分别为 9.84s、0.16s、0.31。在 -O3 中更加清晰:5.71s、0.002s、0.311s。事实上,这似乎很公平,因为编译器可以优化测试 2 和 3 中的“开关”,而它必须执行 test1 中的每个强制转换。这里编译器在内联的时候肯定去掉了test2中的多态性,那么typeid的结果在编译时就知道了。
  • 看了一点生成的代码之后。似乎test2甚至没有生成循环,结果在编译时就知道了。
  • 我在我的回答中发布了一个真正的基准。 Vaughn 基准测试的问题在于,编译器非常擅长识别和提升循环不变量。在循环中一遍又一遍地测试相同的条件与在循环顶部测试一次没有什么不同。如果编译器看到你把result 丢在地上,那就更糟了——在这种情况下根本不需要编译函数!
【解决方案3】:

在几乎所有情况下,您都不需要确切的类型,但您想确保它是给定类型或从它派生的任何类型。如果从它派生的类型的对象不能替换相关类型的对象,那么您就违反了Liskov Substitution Principle,这是正确 OO 设计的最基本规则之一。

【讨论】:

    猜你喜欢
    • 2012-05-13
    • 1970-01-01
    • 1970-01-01
    • 2010-09-20
    • 2013-02-11
    • 2010-09-06
    • 2012-10-01
    • 2010-11-15
    • 2013-06-27
    相关资源
    最近更新 更多