48

在阅读问题之前:
这个问题不是关于使用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_timeLinux 上的方法来获取可用值。
dynamic_cast一次执行了 3 次,测量它们的代码是相同的。

1 次执行的结果如下:
Cast1 持续时间:74 微秒
Cast2 持续时间:2 微秒
Cast3 持续时间:1 微秒

第一次施法总是花费 74-111 微秒,同一执行中的以下施法花费 1-3 微秒。

所以最后我的问题是:
真的dynamic_cast表现不好吗?
根据测试结果它不是。我的测试代码正确吗?
为什么这么多开发人员认为如果不是,它就很慢?

4

6 回答 6

59

首先,您需要测量的性能不仅仅是几次迭代,因为您的结果将取决于计时器的分辨率。尝试例如 100 万+,以建立具有代表性的图片。此外,除非您将其与某些东西进行比较,即进行等效但没有动态转换,否则该结果是没有意义的。

其次,您需要通过优化同一个指针上的多个动态转换来确保编译器不会给您错误的结果(因此使用循环,但每次使用不同的输入指针)。

动态转换会比较慢,因为它需要访问对象的 RTTI(运行时类型信息)表,并检查转换是否有效。然后,为了正确使用它,您需要添加错误处理代码来检查返回的指针是否为NULL. 所有这些都占用了周期。

我知道你不想谈论这个,但“一个经常使用 dynamic_cast 的设计”可能表明你做错了什么......

于 2010-10-29T10:20:46.077 回答
32

如果不比较等效功能,性能就毫无意义。 大多数人说 dynamic_cast 很慢,而不是与等效行为相比。把他们叫出来。换一种方式:

如果“工作”不是必需的,我可以编写比你的失败更快的代码。

有多种实现 dynamic_cast 的方法,有些方法比其他方法更快。例如, Stroustrup 发表了一篇关于使用素数改进 dynamic_cast的论文。不幸的是,控制编译器如何实现强制转换是不寻常的,但如果性能对你来说真的很重要,那么你可以控制你使用的编译器。

然而,不使用dynamic_cast总是比使用它快——但如果你实际上不需要 dynamic_cast,那就不要使用它!如果你确实需要动态查找,那么会有一些开销,然后你可以比较各种策略。

于 2010-10-29T10:22:37.570 回答
26

以下是一些基准测试: http:
//tinodidriksen.com/2010/04/14/cpp-dynamic-cast-performance/
http://www.nerdblog.com/2006/12/how-slow-is-dynamiccast。 html

根据他们的说法,dynamic_cast 比 reinterpret_cast 慢 5-30 倍,而最佳替代方案的性能几乎与 reinterpret_cast 相同。

我将引用第一篇文章的结论:

  • 除了强制转换为基本类型之外,dynamic_cast 的速度很慢;那个特定的演员被优化了
  • 继承级别对 dynamic_cast 有很大影响
  • 成员变量 + reinterpret_cast 是最可靠的
    判断类型的方法;
    但是,这在编码时具有更高的维护开销

单次投射的绝对数字约为 100 ns。像 74 毫秒这样的值似乎并不接近现实。

于 2011-09-28T06:40:51.533 回答
7

很抱歉这么说,但是您的测试对于确定演员阵容是否缓慢几乎没有用处。微秒分辨率远远不够好。我们正在谈论的操作,即使在最坏的情况下,在典型的 PC 上也不应该花费超过 100 个时钟滴答或少于 50 纳秒的时间。

毫无疑问,动态转换将比静态转换或重新解释转换慢,因为在汇编级别上,后两者将相当于一个分配(非常快,1 个时钟滴答的顺序),而动态转换需要代码去检查对象以确定它的真实类型。

我不能直接说它到底有多慢,这可能会因编译器而异,我需要查看为该代码行生成的汇编代码。但是,就像我说的,每次调用 50 纳秒是合理的上限。

于 2010-10-29T10:23:30.303 回答
7

您的里程可能会有所不同,以低估情况。

dynamic_cast 的性能在很大程度上取决于您在做什么,并且可能取决于类的名称是什么(并且,比较相对于时间的时间reinterpet_cast似乎很奇怪,因为在大多数情况下,出于实际目的需要零指令,例如unsigned从投到int)。

我一直在研究它在 clang/g++ 中是如何工作的。假设您dynamic_cast从 aB*到 a 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); } // success;
     if( *curr_typ == typeid(B)) return nullptr;   //failed
     curr_typ = get_direct_base_type_of(*curr_typ); // magic internal operation
}

所以,是的,当*p它实际上是 a时非常快D;只是一个成功type_info的比较。D最坏的情况是演员阵容失败,从到有很多步骤B;在这种情况下,有很多失败的类型比较。

类型比较需要多长时间?它在 clang/g++ 上执行此操作:

compare_eq( type_info const &a, type_info const & b ){
   if( &a == &b) return true;   // same object
   return strcmp( a.name(), b.name())==0;
}

strcmp 是必需的,因为可能有两个不同的字符串对象为type_info.name()同一类型提供(尽管我很确定这只发生在一个在共享库中,而另一个不在该库中时)。但是,在大多数情况下,当类型实际上相等时,它们引用相同的类型名称字符串;因此大多数成功的类型比较都非常快。

name()方法只返回一个指向包含类名称的固定字符串的指针。所以还有另一个因素:如果从D到的许多类的B名称都以 开头MyAppNameSpace::AbstractSyntaxNode<,那么失败的比较将比平时花费更长的时间;strcmp 不会失败,直到它达到损坏的类型名称的差异。

而且,当然,由于整个操作是遍历一组表示类型层次结构的链接数据结构,时间将取决于这些东西是否在缓存中是新鲜的。因此,重复执行相同的演员表可能会显示平均时间,这不一定代表该演员表的典型表现。

于 2019-11-01T02:53:54.447 回答
0

这个问题没有提到替代方案。在 RTTI 被广泛使用之前,或者只是为了避免使用 RTTI,传统的方法是使用虚拟方法来检查类的类型,然后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;
}

于 2021-06-02T09:24:43.033 回答