0

Производительность dynamic_cast?

10

Описание проблемы:

Прежде чем читать вопрос:
Этот вопрос не о полезности использования dynamic_cast. Он касается исключительно его производительности.

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

Я решил проверить производительность этого метода, а не просто верить на слово.

Для тестирования использовался следующий код:

ptime firstValue( microsec_clock::local_time() );

ChildObject* castedObject = dynamic_cast<ChildObject*>(parentObject);

ptime secondValue( microsec_clock::local_time() );
time_duration diff = secondValue - firstValue;
std::cout << "Cast1 lasts:\t" << diff.fractional_seconds() << " microsec" << std::endl;

В приведенном выше коде используются методы из boost::date_time на Linux для получения корректных значений. Я провел 3 вызова dynamic_cast в одном выполнении, и код измерения был одинаковым для всех.

Результаты одного выполнения были следующими:

  • Cast1 lasts: 74 microsec
  • Cast2 lasts: 2 microsec
  • Cast3 lasts: 1 microsec

Первый вызов всегда занимал от 74 до 111 микросекунд, в то время как последующие вызовы в том же выполнении занимали от 1 до 3 микросекунд.

Итак, в итоге у меня возникли следующие вопросы:

  • Является ли dynamic_cast действительно медленным?
  • Согласно моим тестовым результатам, это не так. Правильный ли у меня тестовый код?
  • Почему так много разработчиков думают, что он медленный, если это не так?

5 ответ(ов)

0

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

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

Динамическое приведение типов будет медленнее, потому что оно требует доступа к таблице RTTI (информация о типе во время выполнения) для объекта и проверки, является ли приведение действительным. Кроме того, для правильного использования вам нужно будет добавить обработку ошибок, которая проверяет, является ли возвращаемый указатель NULL. Всё это занимает время на выполнение.

Я знаю, что вы не хотели об этом говорить, но «дизайн, в котором активно используется dynamic_cast» вероятно, указывает на то, что вы делаете что-то не так...

0

Вот несколько замеров производительности:

http://tinodidriksen.com/2010/04/14/cpp-dynamic-cast-performance/

http://www.nerdblog.com/2006/12/how-slow-is-dynamiccast.html

Согласно этим статьям, dynamic_cast в 5-30 раз медленнее, чем reinterpret_cast, а лучшая альтернатива работает почти так же быстро, как reinterpret_cast.

Позвольте процитировать вывод из первой статьи:

  • dynamic_cast медленный, если не приводит к базовому типу; этот конкретный каст оптимизирован.
  • Уровень наследования существенно влияет на производительность dynamic_cast.
  • Использование переменной-члена с reinterpret_cast является самым быстрым надежным способом определить тип; тем не менее, это требует значительно большего времени на обслуживание в процессе написания кода.

Чистые значения замеров находятся на уровне 100 нс для одного приведения. Значения, такие как 74 мс, не кажутся реальными.

0

Ваши результаты могут отличаться, мягко говоря.

Производительность dynamic_cast во многом зависит от того, что именно вы делаете, и может варьироваться в зависимости от названий классов (об comparison с reinterpret_cast тоже стоит упомянуть, так как в большинстве случаев он вообще не требует инструкции для практических целей, как, например, каст из unsigned в int).

Я изучал, как это работает в clang/g++. Предположим, вы осуществляете dynamic_cast из B* в D*, где B является (прямым или косвенным) базовым классом для D, и игнорируем проблемы с множественным наследованием. Кажется, он работает, вызывая библиотечную функцию, которая делает что-то вроде следующего:

for dynamic_cast<D*>(p) where p is B*

type_info const * curr_typ = &typeid(*p);
while (1) {
    if (*curr_typ == typeid(D)) { return static_cast<D*>(p); } // успех
    if (*curr_typ == typeid(B)) return nullptr;   // неудача
    curr_typ = get_direct_base_type_of(*curr_typ); // магическая внутренняя операция
}

Таким образом, да, это довольно быстро, когда *p действительно является D; всего одно успешное сравнение type_info. Наихудший случай – это когда каст неудачен, и существует много шагов от фактического типа до B; в этом случае выполняется множество неудачных сравнений типов.

Сколько времени занимает сравнение типов? В clang/g++ это выполняется следующим образом:

compare_eq(type_info const &a, type_info const &b) {
    if (a.name() == b.name()) return true; // тот же самый объект строки
    return strcmp(a.name(), b.name()) == 0; // та же строка
}

strcmp необходим, поскольку возможно наличие двух различных строковых объектов, предоставляющих type_info.name() для одного и того же типа (хотя я уверен, что это происходит только в случае, если один из них находится в общей библиотеке, а другой – нет). Но в большинстве случаев, когда типы действительно равны, они ссылаются на одну и ту же строку имени типа; таким образом, большинство успешных сравнений типов выполняются очень быстро.

Метод name() просто возвращает указатель на фиксированную строку, содержащую искаженное имя класса. Поэтому есть еще один фактор: если многие из классов на пути от D к B имеют имена, начинающиеся с MyAppNameSpace::AbstractSyntaxNode<, то неудачные сравнения займут больше времени, чем обычно; strcmp не завершит сравнение, пока не дойдет до различия в искаженных названиях типов.

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

0

К сожалению, ваш тест практически бесполезен для определения того, насколько медленен данный каст. Разрешение в микросекундах далеко не достаточное. Мы говорим об операции, которая даже в самом плохом случае не должна занимать больше, скажем, 100 тактов процессора, или менее 50 наносекунд на типичном ПК.

Не вызывает сомнений, что динамическое приведение (dynamic cast) будет медленнее, чем статическое (static cast) или переопределенное (reinterpret cast), так как на уровне ассемблера последние два приведут к присваиванию (очень быстро, порядка 1 такта процессора), в то время как динамическое приведение требует от кода проверки объекта, чтобы определить его реальный тип.

Не могу точно сказать, насколько оно медленно, это, вероятно, будет варьироваться от компилятора к компилятору; мне нужно увидеть сгенерированный ассемблерный код для этой строки. Но, как я уже сказал, 50 наносекунд на вызов — это верхний предел того, что можно считать разумным.

0

Вопрос не упоминает альтернативный подход.

До широкого внедрения RTTI или для избегания его использования традиционным методом в C++ было использовать виртуальный метод для проверки типа класса и последующий static_cast как подходящее решение. У этого метода есть недостаток — он не работает с множественным наследованием, но с другой стороны, он не тратит время на проверку иерархии множественного наследования, что может привести к улучшению производительности.

В моих тестах:

  • dynamic_cast выполняется за около 14.4953 наносекунд.
  • Проверка через виртуальный метод и использование static_cast выполняется примерно в два раза быстрее, 6.55936 наносекунд.

Эти результаты были получены при тестировании с соотношением 1:1 допустимых и недопустимых преобразований, используя следующий код с отключенной оптимизацией. Я использовал Windows для проверки производительности.

#include <iostream>
#include <windows.h>

struct BaseClass
{
    virtual int GetClass() volatile
    { return 0; }
};

struct DerivedClass final : public BaseClass
{
    virtual int GetClass() volatile final override
    { return 1; }
};

volatile DerivedClass *ManualCast(volatile BaseClass *lp)
{
    if (lp->GetClass() == 1)
    {
        return static_cast<volatile DerivedClass *>(lp);
    }
    return nullptr;
}

LARGE_INTEGER perfFreq;
LARGE_INTEGER startTime;
LARGE_INTEGER endTime;

void PrintTime()
{
    float seconds = static_cast<float>(endTime.LowPart - startTime.LowPart) / static_cast<float>(perfFreq.LowPart);
    std::cout << "T=" << seconds << std::endl;
}

BaseClass *Make()
{
    return new BaseClass();
}

BaseClass *Make2()
{
    return new DerivedClass();
}

int main()
{
    volatile BaseClass *base = Make();
    volatile BaseClass *derived = Make2();
    int unused = 0;
    const int t = 1000000000;

    QueryPerformanceFrequency(&perfFreq);
    QueryPerformanceCounter(&startTime);

    for (int n = 0; n < t; ++n)
    {
        volatile DerivedClass *alpha = dynamic_cast<volatile DerivedClass *>(base);
        volatile DerivedClass *beta = dynamic_cast<volatile DerivedClass *>(derived);
        unused += alpha ? 1 : 0;
        unused += beta ? 1 : 0;
    }

    QueryPerformanceCounter(&endTime);
    PrintTime();
    QueryPerformanceCounter(&startTime);

    for (int n = 0; n < t; ++n)
    {
        volatile DerivedClass *alpha = ManualCast(base);
        volatile DerivedClass *beta = ManualCast(derived);
        unused += alpha ? 1 : 0;
        unused += beta ? 1 : 0;
    }

    QueryPerformanceCounter(&endTime);
    PrintTime();

    std::cout << unused;

    delete base;
    delete derived;
}

Таким образом, использование виртуальных методов и static_cast может стать более эффективным для некоторых сценариев, особенно если множественное наследование не является проблемой в вашем проекте.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь