2012-03-31 6 views
7

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

Любые идеи, почему это?

EDITED: создание объекта производится в функции separete сейчас, поэтому compler не знает этого реального типа. Почти такой же результат.

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

+1

Возможно, вам нужно запустить намного больше итераций, чтобы получить достойную статистическую меру. Собираетесь ли вы с максимальной настройкой оптимизации? –

+0

Я пробовал много итераций, но результат тот же. Все оптимизации отключены. Я использовал MSVS2008. –

+4

Ваш тест недействителен, потому что компилятор может легко оптимизировать как виртуальный вызов (в нетривиальный вызов), так и dynamic_cast (в основном noop), потому что он знает, что 'ptr' действительно указывает на объект' Derived'. –

ответ

11

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

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

Более подробная информация доступна in this document.

+0

Итак, если я расширяю класс hierarhy, время dynamic_cast еще больше увеличит? –

+4

@Dmitry, как он реализован, определяется вашей реализацией, но как обобщение да, это правильно. сложность наследования (количество оснований и использование множественного наследования) обычно там, где вводится стоимость. если классы не связаны, ваша реализация может иметь хорошую оптимизацию для этого случая. также обратите внимание на то, что существует несколько краевых случаев, которые увеличивают сложность - 'dynamic_cast' может завершиться неудачей, когда единственная база не может быть определена, поскольку существуют две общие базы. поэтому перед возвратом должна быть проверена вся иерархия. – justin

+0

Страница 31 из связанного документа содержит соответствующие сведения о виртуальном вызове в сравнении с различными dynamic_cast для нескольких компиляторов (хотя я не мог найти, где он рассказал вам, какие компиляторы были использованы). – paxos1977

3

Вы только что измеряете стоимость dynamic_cast<>. Он реализуется с помощью RTTI, что необязательно в любом компиляторе на C++. Project + Properties, C/C++, Language, Enable Run-Time Type Info. Измените его на №

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

4

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

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

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

0

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

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

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

Смежные вопросы