【问题标题】:Performance of dynamic_cast?dynamic_cast 的性能?
【发布时间】:2010-10-29 10:14:31
【问题描述】:

在阅读问题之前:
这个问题不是关于使用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 的表现真的很差吗?
根据测试结果它不是。我的测试代码正确吗?
为什么这么多开发人员认为如果不是这样就很慢?

【问题讨论】:

  • 我错过了什么吗?我看不到 cast2 或 cast3 的任何代码。
  • 谁能说什么不好?您的程序总体表现是否足够好?如果是这样,那么性能还不错。动态转换的总时间是否占执行时间的很大比例?如果没有,那就先考虑其他的事情。更一般地说,对于某些应用程序来说,74 微秒非常慢——在我的上一份工作中,我会在一半的时间内收到并解析来自证券交易所的完整更新记录,更新数据库并告诉客户端应用程序。如果您有兴趣,请将其与其他方法进行比较以获得相同的行为。
  • 代码中有很多 dynamic_casts 是设计问题的明确指标。
  • 查看您运行的完整“最小工作示例”会很有帮助,以便我们可以重复和修改您的测试。
  • 我惊人的阅读你的想法和理解你如何为 Cast2 和 Cast3 创造时间的能力让我推断它会rain herring in Iceland tonight。可编译的代码为王。 PS.Most 转换将是隐式的(将子对象传递给接受父对象(指针/引用)的函数)。聚苯乙烯。你拿它跟什么比较?

标签: c++ performance dynamic-cast boost-date-time


【解决方案1】:

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

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

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

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

【讨论】:

  • +1,但 10K 迭代可能还不够。像 1 亿这样更好。
  • @Oliver Charlesworth "...为了正确使用它,您需要添加错误处理代码来检查返回的指针是否为 NULL" 您提到的检查的类似版本是存在于查找对象运行时类型的每个方法中,因此这不是参数。
  • 幸好你说“大概”,因为clang的实现本身就充满了dynamic_cast。这并没有错,这只是使用充满异构节点类型的通用 AST 的方式。不是每个人都像 Liskov 原则那样使用继承,但它仍然有意义。
  • 嗨,我知道那是 10 年前的事了,但你能告诉我什么是 dynamic_cast 的替代品吗?我写解释器只是为了好玩,一切都是对象。如何在没有 dynamic_cast 的情况下测试例如参数的类型? (枚举无济于事)
  • @hazer_hazer 看看 std::type_info
【解决方案2】:

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

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

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

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

【讨论】:

  • +1。是的,顺便说一句,每个活着的人最终都会死去。这并不意味着活着是个坏主意。
  • “如果性能对你来说真的很重要,那么你就可以控制你使用的编译器。” :::cries 控制台视频游戏开发者的眼泪:::
【解决方案3】:

以下是一些基准测试:
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 毫秒这样的值似乎并不接近现实。

【讨论】:

  • 他得到的值是 74 微秒(微秒),而不是 74 毫秒(毫秒)。即便如此,听起来还是不太现实。
  • “比 reinterpret_cast 慢”的比较没有意义,因为 reinterpret_cast 是编译时(不转换为任何机器代码)而 dynamic_cast 是运行时功能。与零成本操作相比,一切都无限慢。实际比较是针对基准循环本身。显然,结果取决于基准循环中完成的工作。现在的问题变成了:基准循环做了什么?阅读源代码,它对同一个对象重复做同样的转换(不太现实的IMO),调用一个虚函数,并添加一个数字。
【解决方案4】:

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

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

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

【讨论】:

  • dynamic_cast 需要访问 RTTI,这需要循环。
【解决方案5】:

为了轻描淡写,您的里程可能会有所不同。

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

我一直在研究它在 clang/g++ 中的工作原理。假设您是从B*D*dynamic_cast,其中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&lt; 开头,那么失败的比较将比平时花费更长的时间; strcmp 不会失败,直到它在重整类型名称中出现差异。

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

【讨论】:

  • 我发现这个答案是对这个九岁问题的一个很好的补充:)
  • @JoelBodenmann 我认为事情发生了变化;大约 10 年前,我正在研究使用动态转换的东西,当一些代码进入共享库时,它们就崩溃了;我必须做各种事情来确保共享库中的类型信息不会在其他代码中复制。因此,似乎添加了 strcmp 比较以更好地支持这一点。从那以后的这些年里,我还没有遇到过动态转换和共享库的交集,所以当这种变化发生时我不会注意到。
  • 有人能详细说明一下在 clang/g++ 中查找 type_info 是如何实现的吗?
【解决方案6】:

这个问题没有提到替代方案。 在 RTTI 广泛可用之前,或者只是为了避免使用 RTTI,传统的方法是使用虚拟方法检查类的类型,然后酌情使用static_cast。这样做的缺点是它不适用于多重继承,但优点是它也不必花时间检查多重继承层次结构!

在我的测试中:

  • dynamic_cast 的运行时间约为 14.4953 纳秒
  • 检查虚拟方法和static_casting 以大约两倍的速度运行,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;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-11-10
    • 2015-06-27
    • 2011-03-10
    • 2023-03-18
    • 2011-10-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多