【问题标题】:Runtime overhead of shared_ptr for subclass destruction compared to virtual destructor与虚拟析构函数相比,用于子类销毁的 shared_ptr 的运行时开销
【发布时间】:2017-04-25 11:40:58
【问题描述】:

我在 Youtube 视频 (https://www.youtube.com/watch?v=ZiNGWHg5Z-o&list=PLE28375D4AC946CC3&index=6) 上遇到了 shared_ptr 模仿虚拟析构函数行为的技巧,在搜索互联网时发现了这个 SO 答案:shared_ptr magic :)

通常情况下,如果 B 继承自 A 并且有自己的析构函数,我们需要在基类 A 中添加一个虚拟析构函数来确保正确调用 B 的析构函数。但是,使用 shared_ptr 可以避免对虚拟析构函数的需求。

由于多态函数中的 vtable 查找存在运行时开销,我很想知道 shared_ptr 技巧是否可以避免这种开销。

【问题讨论】:

  • 请注意std::shared_ptr 确实有原子引用计数开销。这可能会或可能不会超过虚拟机制。
  • 为什么不试试测量呢?

标签: c++ performance oop c++11 shared-ptr


【解决方案1】:

首先,unique_ptr 可以实现“技巧”。您只需提供一个类型擦除删除器,如下所示。

但是,如果您查看实现,您当然会发现它涉及通过函数指针的调用 - 这正是虚拟析构函数在幕后所做的。

虚拟函数调用并不昂贵。它们只涉及一次内存提取。如果您在一个紧密的循环中执行此操作,这是唯一一次出现性能问题,那么该提取几乎肯定会被缓存。

此外,如果编译器可以证明它知道正确的析构函数,它将完全忽略多态查找(当然,优化是打开的)。

长话短说,如果这是您唯一的性能问题,那么您就没有性能问题。如果您有性能问题并且您认为这是因为虚拟析构函数,那么您肯定错了。

示例代码:

#include <iostream>
#include <memory>

struct A {
    ~A() { std::cout << "~A\n"; }
};

struct B : A {
    ~B() { std::cout << "~B\n"; }
};


struct poly_deleter {
    template<class T>
    struct tag {
    };

    template<class T>
    static void delete_it(void *p) { delete reinterpret_cast<T *>(p); }

    template<class T>
    poly_deleter(tag<T>) : deleter_(&delete_it<T>) {}

    void operator()(void *p) const { deleter_(p); }

    void (*deleter_)(void *) = nullptr;
};

template<class T> using unique_poly_ptr = std::unique_ptr<T, poly_deleter>;

template<class T, class...Args> auto make_unique_poly(Args&&...args) -> unique_poly_ptr<T>
{
    return unique_poly_ptr<T> {
            new T (std::forward<Args>(args)...),
            poly_deleter(poly_deleter::tag<T>())
    };
};

int main()
{

    auto pb = make_unique_poly<B>();

    auto pa = std::move(pb);

    pa.reset();
}

预期输出:

~B
~A

【讨论】:

  • 我现在明白了,谢谢。我担心的性能开销是由于 vtable 查找而导致的分支预测丢失。我并不关心 vtable 查找开销(无论如何可能会被缓存),而是关心分支预测如何受到多态性的影响。但是正如您所说,编译器优化在正常用例中应该能够完全省略查找,这也可以解决分支预测问题。
  • @Anton 在过于担心潜在的性能问题之前,您应该衡量一下是否真的有任何问题。
  • @Anton 间接查找不会导致分支预测丢失。不涉及条件分支。不要害怕:)
【解决方案2】:

Shares ptr 类型擦除析构函数;各种类型的擦除往往都有“相似”的成本;给或取两倍。有些保存缓存未命中或两次。一种类型擦除是虚函数表。

共享 ptr 如何清除销毁留给实现。但与内联函数调用相比,类型擦除永远不会完全免费。

很难证明哪个更有效,因为缓存耗尽可能比您尝试的任何微基准测试更重要。在这两种情况下,它都不太可能成为导致减速的主要原因。

【讨论】:

    猜你喜欢
    • 2016-11-10
    • 1970-01-01
    • 2011-12-22
    • 1970-01-01
    • 2018-05-02
    • 2017-07-30
    • 1970-01-01
    • 2011-01-19
    • 2018-01-02
    相关资源
    最近更新 更多