Какова стоимость typeid? - PullRequest
       22

Какова стоимость typeid?

10 голосов
/ 11 ноября 2011

Я рассматриваю настройку удаления типа, в которой для определения типа используется 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 (), потому что для того, чтобы удостовериться, что идентификаторы уникальны и детерминированны, требуется немало хакеров.

Ответы [ 3 ]

11 голосов
/ 28 июня 2017

В качестве альтернативы BaseThing может иметь чисто виртуальный GetID(), который будет использоваться для определения типа вместо использования typeid.В этой ситуации, с только 1 уровнем наследования, какова стоимость typeid против стоимости вызова виртуальной функции?Я знаю, что typeid каким-то образом использует vtable, но как именно он работает?

В Linux и Mac или что-то еще, использующее ABI Itanium C ++, typeid(x) компилируется в две инструкции загрузки - он просто загружаетvptr (то есть адрес некоторой виртуальной таблицы) из первых 8 байтов объекта x, а затем загружает -1-й указатель из этой виртуальной таблицы.Этот указатель &typeid(x).Это на один вызов функции дешевле дороже, чем вызов виртуального метода.

В Windows он включает в себя порядка четырех инструкций загрузки и пару (незначительных)ALU ops, потому что Microsoft C ++ ABI немного более корпоративный .( source ) Честно говоря, это может оказаться на уровне виртуального вызова метода.Но это все еще очень дешево по сравнению с dynamic_cast.

A dynamic_cast включает вызов функции в среду выполнения C ++, которая имеет lot загрузок и условных ветвей и тому подобное.

Так что да, использование typeid будет намного быстрее, чем dynamic_cast.Это будет правильный для вашего варианта использования? - это сомнительно.(См. Другие ответы о заменяемости по Лискову и тому подобное.) Но это будет быстро? - да.

Здесь я взял код теста игрушек из высоко оцененного ответа Вона и сделал егов фактический бенчмарк , избегая очевидной оптимизации подъема петли, которая мешала ему все время.Результат для libc ++ abi на моем Macbook:

$ 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» просто показывает, что он все время работает, и нетвремя ожидания, а «Итерации» - это просто количество прогонов, которое потребовалось, чтобы получить хороший предел погрешности.)

Вы можете видеть, что typeid перебивает dynamic_cast даже при -O0, но когда вывключите оптимизацию, это работает даже лучше - потому что компилятор может оптимизировать любой код, который you пишет. Весь этот уродливый код, спрятанный внутри libc ++, функция __dynamic_cast abi не может быть оптимизирована компилятором так, как это уже было, поэтому включение -O3 не сильно помогло.

5 голосов
/ 11 ноября 2011

Как правило, вы не просто хотите знать тип, но и что-то делаете с объектом в качестве этого типа.В этом случае 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 - самый быстрый метод при использовании оптимизации с этим компилятором.

2 голосов
/ 11 ноября 2011

Почти во всех случаях вам не нужен точный тип, но вы хотите убедиться, что он относится к данному типу или к любому типу, производному от него . Если объект типа, производного от него, не может быть заменен объектом рассматриваемого типа, то вы нарушаете Принцип замены Лискова , который является одним из самых фундаментальных правил правильного проектирования ОО.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...