Почему вызов виртуальной функции быстрее, чем dynamic_cast? - PullRequest
8 голосов
/ 01 апреля 2012

Я написал простой пример, который оценивает среднее время вызова виртуальной функции, используя интерфейс базового класса и dynamic_cast и вызов не виртуальной функции. Вот оно:

#include <iostream>
#include <numeric>
#include <list>
#include <time.h>

#define CALL_COUNTER (3000)

__forceinline int someFunction()
{
  return 5;
}

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

struct Derived : public Base
{
  Derived(){};
  virtual ~Derived(){};
  virtual int virtualCall(){ return someFunction(); };
  int notVirtualCall(){ return someFunction(); };
};


struct Derived2 : public Base
{
  Derived2(){};
  virtual ~Derived2(){};
  virtual int virtualCall(){ return someFunction(); };
  int notVirtualCall(){ return someFunction(); };
};

typedef std::list<double> Timings;

Base* createObject(int i)
{
  if(i % 2 > 0)
    return new Derived(); 
  else 
    return new Derived2(); 
}

void callDynamiccast(Timings& stat)
{
  for(unsigned i = 0; i < CALL_COUNTER; ++i)
  {
    Base* ptr = createObject(i);

    clock_t startTime = clock();

    for(int j = 0; j < CALL_COUNTER; ++j)
    {
      Derived* x = (dynamic_cast<Derived*>(ptr));
      if(x) x->notVirtualCall();
    }

    clock_t endTime = clock();
    double callTime = (double)(endTime - startTime) / CLOCKS_PER_SEC;
    stat.push_back(callTime);

    delete ptr;
  }
}

void callVirtual(Timings& stat)
{
  for(unsigned i = 0; i < CALL_COUNTER; ++i)
  {
    Base* ptr = createObject(i);

    clock_t startTime = clock();

    for(int j = 0; j < CALL_COUNTER; ++j)
      ptr->virtualCall();


    clock_t endTime = clock();
    double callTime = (double)(endTime - startTime) / CLOCKS_PER_SEC;
    stat.push_back(callTime);

     delete ptr;
  }
}

int main()
{
  double averageTime = 0;
  Timings timings;


  timings.clear();
  callDynamiccast(timings);
  averageTime = (double) std::accumulate<Timings::iterator, double>(timings.begin(), timings.end(), 0);
  averageTime /= timings.size();
  std::cout << "time for callDynamiccast: " << averageTime << std::endl;

  timings.clear();
  callVirtual(timings);
  averageTime = (double) std::accumulate<Timings::iterator, double>(timings.begin(), timings.end(), 0);
  averageTime /= timings.size();
  std::cout << "time for callVirtual: " << averageTime << std::endl;

  return 0;
}

Похоже, что callDynamiccast занимает почти в два раза больше.

time for callDynamiccast: 0.000240333

time for callVirtual: 0.0001401

Есть идеи, почему?

РЕДАКТИРОВАНИЕ: создание объекта теперь выполняется в отдельной функции, поэтому компилятор не знает его реального типа. Почти тот же результат.

EDITED2: создать два разных типа производных объектов.

Ответы [ 4 ]

12 голосов
/ 01 апреля 2012

Вызов виртуальной функции похож на указатель на функцию или, если компилятор знает тип, статическая диспетчеризация. Это постоянное время.

dynamic_cast совершенно другой - он использует определенные для реализации средства для определения типа. Это не постоянное время, может пересечь иерархию классов (также учитывать множественное наследование) и выполнить несколько поисков. Реализация может использовать сравнения строк. Следовательно, сложность выше в двух измерениях. По этим причинам системы реального времени часто избегают / не поощряют dynamic_cast.

Более подробная информация доступна в этом документе .

4 голосов
/ 01 апреля 2012

Следует отметить, что цель виртуальных функций состоит в том, чтобы не создавать граф наследования. Виртуальные функции существуют, так что вы можете использовать экземпляр производного класса, как если бы он был базовым классом. Так что более специализированные реализации функций могут быть вызваны из кода, который первоначально назывался версиями базового класса.

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

Так что нет никаких оснований ожидать, что dynamic_cast + вызов будет быстрее.

3 голосов
/ 01 апреля 2012

Вы просто измеряете стоимость dynamic_cast<>.Это реализовано с RTTI, это необязательно в любом компиляторе C ++.Проект + Свойства, C / C ++, Язык, Включить параметр времени выполнения Тип информации.Измените его на Нет.

Теперь вы получите неуловимое напоминание о том, что dynamic_cast<> больше не может выполнять надлежащую работу.Произвольно измените его на static_cast<>, чтобы получить радикально разные результаты.Ключевым моментом здесь является то, что если вы знаете , что upcast всегда безопасен, то static_cast<> покупает вам производительность, которую вы ищете.Если вы не знаете за факт, что угроза безопасности безопасна, то dynamic_cast<> удерживает вас от неприятностей.Это та проблема, которую безумно трудно диагностировать.Обычный режим сбоев - повреждение кучи, вы сразу получаете GPF, если вам действительно повезло.

0 голосов
/ 04 апреля 2017

Разница в том, что вы можете вызывать виртуальную функцию в любом экземпляре, производном от Base. Элемент notVirtualCall() не существует в Base и не может быть вызван без предварительного определения точного динамического типа объекта.

Следствием этого различия является то, что vtable базового класса включает в себя слот для virtualCall(), который содержит указатель функции на правильную функцию для вызова. Таким образом, виртуальный вызов просто преследует указатель vtable, включенный в качестве первого (невидимого) члена всех объектов типа Base, загружает указатель из слота, соответствующего virtualCall(), и вызывает функцию, стоящую за этим указателем.

Когда вы делаете dynamic_cast<>, класс Base, напротив, не знает во время компиляции, что другие классы в итоге получат из него. Следовательно, он не может включать в свой vtable информацию, которая облегчает разрешение dynamic_cast<>. Именно недостаток информации делает реализацию dynamic_cast<> более дорогой, чем вызов виртуальной функции. dynamic_cast<> должен на самом деле искать в дереве наследования реального объекта, чтобы проверить, найден ли тип назначения приведения среди его баз. Это работа, которую избегает виртуальный вызов.

...