dynamic_cast的性能表现?

52
在阅读问题之前:
这个问题不涉及使用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;

上述代码使用Linux下的boost::date_time方法来获取可用值。
我在一次执行中进行了3个dynamic_cast,测量它们的代码相同。
1次执行的结果如下:
Cast1持续时间:74微秒
Cast2持续时间:2微秒
Cast3持续时间:1微秒
第一个转换始终需要74-111微秒,在同一次执行中的后续转换需要1-3微秒。
所以最后我的问题是:
dynamic_cast真的表现不佳吗?根据测试结果并非如此。我的测试代码正确吗?为什么这么多开发人员认为它很慢,而实际上并不是呢?

26
我有所疑惑,我找不到cast2或cast3的任何代码。 - Flexo
7
谁能说什么是不好的?如果您的程序总体表现良好,那么它的性能就不会差。如果动态转换的总时间不占执行时间的很大比例,那么首先要担心其他事情。更普遍地说,对于某些应用程序来说,74微秒的速度非常慢——在我上一份工作中,我会在半个时间内接收和解析来自证券交易所的整个更新记录,并更新数据库并告诉客户端应用程序有关此事。如果您感兴趣,可以将其与获得相同行为的其他方法进行比较。 - Tony Delroy
4
代码里有很多dynamic_cast是设计问题的明确指标。 - BЈовић
2
能否提供一个完整的“最小工作示例”,以便我们可以重复和修改您的测试,这将非常有帮助。 - Flexo
4
我惊人的能力可以读取你的思维并理解你是如何生成Cast2和Cast3的时间,这让我得出结论:今晚冰岛将会“下鲱鱼雨”。可编译代码是王道。PS. 大多数强制转换都是隐式的(将子对象传递给接受父对象的函数(指针/引用))。PPS. 你在将其与什么进行比较? - Martin York
显示剩余4条评论
6个回答

64

首先,你需要进行更多次数的性能测量,而不仅仅是几次迭代,因为你的结果将被计时器的分辨率所支配。试试使用100万次或更多次,以建立一个代表性的图像。此外,除非你将其与某些东西进行比较(即执行相同操作但不使用动态转换),否则此结果毫无意义。

其次,你需要确保编译器没有通过优化同一指针上的多个动态转换而给出虚假结果(因此使用循环,但每次使用不同的输入指针)。

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

我知道你不想谈论这个问题,但“大量使用dynamic_cast的设计”可能表明你正在做一些错误的事情......


8
+1,但10000次迭代可能不够。使用一亿次迭代会更好。 - sharptooth
“...为了正确使用它,您需要添加错误处理代码来检查返回的指针是否为NULL。”你提到的检查类似于在查找对象的运行时类型的每个方法中都存在,因此这不是一个论点。 - luke1985
3
很幸运你使用了“可能”的词语,因为clang本身的实现充满了dynamic_cast。这并不是错误,这只是在处理一个充满异构节点类型的通用AST时的一种工作方式。并非所有人都像Liskov原则中那样使用继承,但它仍然有意义。 - v.oddou
嗨,我知道这是10年前的事了,但你能告诉我dynamic_cast的替代方法吗?我只是为了好玩写一个解释器,其中一切都是对象。如果没有dynamic_cast,我该如何测试例如参数的类型?(枚举不起作用) - hazer_hazer
@hazer_hazer 看一下 std::type_info。 - dev.dmtrllv

34

没有比较等效功能更无意义的性能表现。 大多数人认为dynamic_cast很慢,但没有将其与等效行为进行比较。要揭穿他们。换句话说:

如果“可行”不是一个要求,我可以编写比你更快失败的代码。

有各种实现dynamic_cast的方法,有些比其他方法更快。例如,Stroustrup发表了一篇关于使用质数优化dynamic_cast的论文。不幸的是,控制编译器如何实现强制类型转换通常是不寻常的,但是如果性能真的很重要,则可以控制使用哪个编译器。

然而,不使用dynamic_cast将始终比使用它快 - 但如果您实际上不需要dynamic_cast,则不要使用它! 如果确实需要动态查找,则会有一些开销,并且您可以比较各种策略。


9
是的,顺便说一下,每个活着的人最终都会死去。这并不意味着活着是一个坏主意。 - sharptooth
如果性能真的对你很重要,那么你可以控制使用哪个编译器。:::流泪控制台视频游戏开发者的眼泪::: - jwd

29

这里有一些基准测试:


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毫秒这样的值似乎不接近现实。


4
他得到的值是74微秒(usec),而不是74毫秒(msec)。即便如此,这仍然听起来不太现实。 - Ponkadoodle
2
“比reinterpret_cast慢”这种比较是没有意义的,因为reinterpret_cast是编译时完成的(不会转换成任何机器代码),而dynamic_cast是运行时特性。与零成本操作相比,所有操作都会无限变慢。实际的比较是针对基准循环本身的。显然,结果取决于基准循环中所做的工作。现在的问题是:基准循环做了什么?阅读源代码,它反复对同一对象进行相同的转换(在我看来不太现实),调用虚函数并添加一个数字。 - Michael Franzl

11

你的结果可能因情况而异,这是低调陈述。

dynamic_cast 的性能在很大程度上取决于你正在做什么,也可能取决于类名(同时,相对于 reinterpret_cast 比较时间似乎很奇怪,因为在大多数情况下,这需要零条指令才能实现,例如从 unsigned 转换为 int)。

我一直在研究clang/g++中的工作原理。假设你正在将 B* 转换为 D*,其中 BD 的一个(直接或间接)基类,并忽略多重继承的复杂性,它似乎通过调用一个库函数来完成以下操作:

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实际上是一个D时,速度非常快;只需要一次成功的type_info比较。

最糟糕的情况是当强制类型转换失败,从DB有很多步骤,这种情况下会有很多失败的类型比较。

类型比较需要多长时间?在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()方法只返回指向包含类的名称的修饰名称的固定字符串的指针。 因此,还有另一个因素:如果从DB的许多类的名称都以MyAppNameSpace :: AbstractSyntaxNode<开头,则失败的比较将花费比平常更长的时间; 在找到编码类型名称的差异之前,strcmp函数不会失败。

当然,由于整个操作遍历了表示类型层次结构的一堆链接数据结构,因此时间取决于这些东西是否在缓存中更新。 因此,重复执行的同一强制转换可能显示出不代表该强制转换典型性能的平均时间。


1
我认为这个答案是对这个九年老问题的很好补充 :) - Joel Bodenmann
@JoelBodenmann 我认为情况已经改变了;大约10年前我在处理某个使用动态转化的项目时,当一些代码进入共享库时这些转换出现了问题;我必须采取各种措施确保共享库中的类型信息不会在其他代码中复制。所以看起来strcmp比较被添加是为更好地支持它。自那时以来,我没有遇到动态转换和共享库的交集,所以当这个修改发生时我没有注意到。 - greggo
有人能详细说明一下在clang/g++中如何实现type_info的查找吗? - andoryu-

7
抱歉要说这句话,但是你的测试几乎没有用于确定转换是否缓慢。微秒分辨率远远不够好。我们谈论的是一项操作,即使在最坏的情况下,也不应该超过100个时钟周期,或者在典型PC上不到50纳秒。
毫无疑问,动态转换将比静态转换或重新解释转换慢,因为在汇编级别上,后两者将相当于赋值(非常快,约1个时钟周期),而动态转换需要代码去检查对象以确定其实际类型。
我无法立即说出它有多慢,这可能会因编译器而异,我需要看到为该行代码生成的汇编代码。但是,正如我所说,每次调用50纳秒是我们期望合理的上限。

1
dynamic_cast 需要访问 RTTI,这将会消耗一些时间。 - doron

3
问题并没有提到替代方案。 在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;
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接