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

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


person David    schedule 11.11.2011    source источник
comment
Первоначально виртуальное наследование было разработано для того, чтобы избежать переключения или цепочки if-else, которые вы используете в своей основной функции. Этот переключатель ужасно подвержен ошибкам, если кто-то забывает добавить else. Вы абсолютно уверены, что виртуальная отправка или двойная отправка не помогут в вашем случае?   -  person thiton    schedule 11.11.2011
comment
Boost.Any использует typeid (в сочетании с static_cast) в порядке. Однако пользовательский код использует функцию get и не беспокоится о typeid. - На самом деле, вы можете использовать его вместо того, чтобы накатывать свой собственный, поскольку он делает больше для вас (клонирование и управление динамической памятью).   -  person visitor    schedule 11.11.2011
comment
@thiton Рассмотрим большинство существующих сегодня в мире методов обработчиков событий - почти все они имеют переключатель или цепочку if/else, эквивалентную приведенной выше.   -  person David    schedule 11.11.2011
comment
@visitor Boost.Any выглядит идеально, за исключением того, что он не обновлен для ссылок rvalue, он может выиграть от make_any, как у shared_ptr есть make_shared, а any_cast не заменяет должным образом dynamic_cast (см. ответ celtschk ниже, почему).   -  person David    schedule 11.11.2011
comment
@Dave: я не понимаю смысла dynamic_cast. В Boost.Any нет промежуточных типов.   -  person UncleBens    schedule 12.11.2011


Ответы (3)


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

person Vaughn Cato    schedule 11.11.2011
comment
Мне не нужно было бы использовать динамическое приведение, если бы я знал тип - Итак, во-первых, если бы я сделал: Thing<B>* pB = static_cast<Thing<B>*>(pThing);. Использование typeid и static_cast будет намного быстрее, чем n dynamic_casts. - person David; 11.11.2011
comment
@Dave Почему typeid должно быть быстрее, чем dynamic_cast? Конечно, в вашем случае вы знаете тип (из-за typeid), но как только dynamic_cast возвращает не 0 (или не выдает), вы также знаете тип и уже получили свое приведение. - person Christian Rau; 11.11.2011
comment
По словам посетителя (комментатора основного вопроса), я только что сказал, как это делает Boost.Any, хех. typeid будет быстрее, потому что вы делаете это только один раз, а не n раз. - person David; 11.11.2011
comment
@Dave: Да, если много случаев и проблема с производительностью, я понимаю вашу точку зрения. - person Vaughn Cato; 11.11.2011
comment
@Dave: Интересно, что я провел тест производительности и сравнил его с использованием динамического приведения, идентификатора типа и виртуальных функций. Без оптимизации я получил время выполнения 2,27 с, 0,63 с и 0,47 с соответственно. С оптимизацией -O2 я получил время выполнения 0,12 с, 0,22 с и 0,29 с соответственно. Это использует g++ 4.5.1. - person Vaughn Cato; 11.11.2011
comment
@VaughnCato Попробуйте удалить все задания из циклов в каждом из этих тестов, они различаются по количеству и типу в каждом тесте. Кроме того, как выглядит ваш main? Вы вызываете каждый тест 4 раза (по одному для каждого типа)? Это интересные результаты, но почему dynamic_cast работает так быстро? Что вы используете, чтобы рассчитать время? - person David; 11.11.2011
comment
@Dave: Его циклы практически идентичны, за исключением некоторых посторонних переменных в случае dynamic_cast, которые оптимизатор определенно удалил. Что касается того, почему dynamic_cast работает так быстро... вы, по-видимому, ошибочно полагаете, что dynamic_cast работает медленно. Это не так, если вы не используете старинный компилятор. Для неглубокого дерева наследования это так же просто, как сравнение нескольких указателей. И поскольку typeid используется так редко, я не удивлюсь, если обнаружу, что его подпрограммы просто не так хорошо написаны, как подпрограммы dynamic_cast. - person Dennis Zickefoose; 11.11.2011
comment
@Dave: я передаю аргумент командной строки, указывающий, какой тест запустить, а затем проверяю его в main() и вызываю соответствующий тест. Затем я использую команду time для проверки времени выполнения для каждого случая. - person Vaughn Cato; 11.11.2011
comment
Это забавно, потому что я пытался запустить эти яички с g++-4.7, и результат с -O2 был совершенно другим: 9,84 с, 0,16 с, 0,31. а в -О3 еще четче: 5,71 с, 0,002 с, 0,311 с. На самом деле это кажется вполне справедливым, поскольку компилятор может оптимизировать переключение в тестах 2 и 3, в то время как он должен выполнять каждое приведение в тесте 1. Здесь компилятор наверняка избавится от полиморфизма в test2 при встраивании, тогда результат typeid будет известен во время компиляции. - person log0; 05.08.2012
comment
Немного посмотрев на сгенерированный код. Кажется, что test2 даже не генерирует цикл, результат известен во время компиляции. - person log0; 05.08.2012
comment
Move int id = bp-›id(); вне цикла - вы увидите увеличение производительности - person yudjin; 29.11.2016
comment
Я опубликовал реальный тест в своем ответе. Проблема с эталонным тестом Вона заключается в том, что компиляторы действительно очень хороши в идентификации и поднятии инвариантов циклов. Проверка одного и того же условия снова и снова в цикле ничем не отличается от проверки его один раз в начале цикла. А если компилятор увидит, что вы роняете result на пол, это еще хуже — в этом случае вообще не нужно компилировать функцию! - person Quuxplusone; 28.06.2017

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

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

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

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

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

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

person Quuxplusone    schedule 28.06.2017
comment
Благодарю вас! Рад видеть это. Мне было интересно, почему динамическое приведение появилось быстрее в ответе Вона. - person Thor Correia; 04.04.2019

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

person celtschk    schedule 11.11.2011