【问题标题】:Is make_shared really more efficient than new?make_shared 真的比 new 更高效吗?
【发布时间】:2012-03-07 07:41:57
【问题描述】:

我正在用 C++11 中的 shared_ptrmake_shared 进行试验,并编写了一个小玩具示例来查看调用 make_shared 时实际发生的情况。作为基础架构,我使用 llvm/clang 3.0 以及 XCode4 中的 llvm std c++ 库。

class Object
{
public:
    Object(const string& str)
    {
        cout << "Constructor " << str << endl;
    }

    Object()
    {
        cout << "Default constructor" << endl;

    }

    ~Object()
    {
        cout << "Destructor" << endl;
    }

    Object(const Object& rhs)
    {
        cout << "Copy constructor..." << endl;
    }
};

void make_shared_example()
{
    cout << "Create smart_ptr using make_shared..." << endl;
    auto ptr_res1 = make_shared<Object>("make_shared");
    cout << "Create smart_ptr using make_shared: done." << endl;

    cout << "Create smart_ptr using new..." << endl;
    shared_ptr<Object> ptr_res2(new Object("new"));
    cout << "Create smart_ptr using new: done." << endl;
}

现在请看一下输出:

使用 make_shared 创建 smart_ptr...

构造函数 make_shared

复制构造函数...

复制构造函数...

析构函数

析构函数

使用 make_shared 创建 smart_ptr: 完成。

使用 new... 创建 smart_ptr

构造函数新

使用 new: done 创建 smart_ptr。

析构函数

析构函数

make_shared 似乎调用了复制构造函数两次。如果我使用常规newObject 分配内存,则不会发生这种情况,只会构造一个Object

我想知道的是以下内容。听说make_shared应该比new效率更高(1,2)。一个原因是因为make_shared 将引用计数与要在同一内存块中管理的对象一起分配。好的,我明白了。这当然比两个单独的分配操作更有效。

相反,我不明白为什么这必须伴随着两次调用Object 的复制构造函数的代价。因此,我不相信make_shared每个 情况下都比使用new 分配更有效。我在这里错了吗?好吧,可以为Object 实现一个移动构造函数,但我仍然不确定这是否比通过new 分配Object 更有效。至少不是在所有情况下。如果复制Object 比为引用计数器分配内存更便宜,那将是正确的。但是shared_ptr-internal 引用计数器可以使用几种原始数据类型来实现,对吧?

您能帮忙解释一下为什么make_shared 在效率方面是要走的路,尽管概述了复制开销?

【问题讨论】:

  • 你认为auto是什么意思?
  • 在测试 C++11 函数的速度之前,您可能应该在您的类中实现一个 move 构造函数,并使其完全符合 C++11。
  • 该代码与输出不匹配。您显示的代码泄漏。
  • 这会有什么不同,@Ildjarn?此代码中的任何内容都不应触发移动任何Object 实例的副本。该代码根本与报告的输出不匹配。
  • @Rob : 除了“应该”之外,如果没有移动构造函数,可能会出现本来应该是移动的副本,因此在没有移动构造函数的情况下计算副本是没有意义的。

标签: c++ shared-ptr clang libc++ make-shared


【解决方案1】:

作为基础架构,我使用的是 llvm/clang 3.0 以及 XCode4 中的 llvm std c++ 库。

嗯,这似乎是你的问题。 C++11 标准在第 20.7.2.2.6 节中对make_shared&lt;T&gt;(和allocate_shared&lt;T&gt;)提出了以下要求:

要求:表达式 ::new (pv) T(std::forward(args)...),其中 pv 的类型为 void* 并指向适合保存 T 类型的对象的存储,应具有良好的格式. A 应为分配器 (17.6.3.5)。 A 的拷贝构造函数和析构函数不会抛出异常。

T 不需要是可复制构造的。事实上,T 甚至不需要是非放置新可构造的。它只需要可就地构建。这意味着make_shared&lt;T&gt; 唯一能用T 做的事情就是new 它就地。

所以你得到的结果不符合标准。 LLVM 的 libc++ 在这方面被破坏了。提交错误报告。

作为参考,以下是我将您的代码导入 VC2010 时发生的情况:

Create smart_ptr using make_shared...
Constructor make_shared
Create smart_ptr using make_shared: done.
Create smart_ptr using new...
Constructor new
Create smart_ptr using new: done.
Destructor
Destructor

我还把它移植到了Boost原来的shared_ptrmake_shared,得到了和VC2010一样的东西。

我建议提交一个错误报告,因为 libc++ 的行为被破坏了。

【讨论】:

  • "你得到的结果与 C++ 标准允许的完全一致。"我在代码中看不到任何会导致 Object 实例被复制/移动构造的内容(无论编译器是否选择省略这样的构造。)
  • @AndrewDurward:实际上,你是对的,也是错的。标准对make_shared&lt;T&gt; 的要求并未说明T 必须是可复制构造的。因此,make_shared&lt;T&gt;不能调用复制构造函数。如果标准确实允许 T 是可复制构造的,那么您就错了,make_shared&lt;T&gt; 的实现可以调用它。
  • @NicolBolas:感谢针对 libc++ 的错误报告。我同意你的分析。这已在 libc++ public svn trunk 中修复,不再调用复制构造函数。
  • 我隐式删除了复制构造函数,因为我定义了一个用户声明的移动构造函数。现在 clang 抱怨 make_shared 调用隐式删除的复制构造函数。那么如果make_shared 不需要拷贝构造函数,这是不是bug?
  • 我将一个临时文件传递给make_shared,比如std:make_shared(M(..)) 将其更改为std:make_shared(std:move(M(..))),现在很好。
【解决方案2】:

你必须比较这两个版本:

std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));

在您的代码中,第二个变量只是一个裸指针,根本不是共享指针。


现在是肉。 make_shared (实际上)更有效,因为它在一个动态分配中将引用控制块与实际对象一起分配。相比之下,采用裸对象指针的shared_ptr 的构造函数必须为引用计数分配另一个 动态变量。权衡是make_shared(或其表亲allocate_shared)不允许您指定自定义删除器,因为分配是由分配器执行的。

(这不影响对象本身的构造。从Object的角度来看,两个版本没有区别。效率更高的是共享指针本身,而不是托管对象。)

【讨论】:

  • “make_shared(实际上)更高效,因为它在一个动态分配中将引用控制块与实际对象一起分配”。我认为这只是 VS2012 的情况,他们做了这个优化,但是 linux std-libs 没有做那个优化(还没有?)。
  • @Ela782:是的,GCC 已经这样做了一段时间。这是 20.7.2.2.6/6 中的标准明确推荐的。
  • 所以这种效率只发生在shared_ptr的初始化中?后面使用make_shared创建的ptr有没有效率?
  • @bigxiao:不应该。一个好的实现会将指向对象的实际指针存储在 shared_ptr 的开头,而不管 shared_ptr 是如何创建的,因此解引用永远不需要比原始指针更多的计算。当然,最终的释放是不同的(尤其是在存在弱指针的情况下)。
  • @KerrekSB make_shared 将对象和引用计数存储在一起(在同一个控制块中),因此缓存未命中可能更少。
【解决方案3】:

所以要记住的一件事是您的优化设置。如果没有启用优化,测量性能,尤其是关于 c++ 的性能是毫无意义。我不知道您是否确实进行了优化编译,所以我认为值得一提。

也就是说,您使用此测试测量的内容不是make_shared 更有效的方式。简而言之,您测量的是错误的东西:-P。

这是交易。通常,当您创建共享指针时,它至少有 2 个数据成员(可能更多)。一个用于指针,一个用于引用计数。这个引用计数是在堆上分配的(这样它就可以在具有不同生命周期的shared_ptr 之间共享......毕竟这就是重点!)

因此,如果您要创建具有类似 std::shared_ptr&lt;Object&gt; p2(new Object("foo")); 的对象,则至少有 2 次调用 new。一个用于Object,一个用于引用计数对象。

make_shared 有一个选项(我不确定它是否必须),做一个new,它足够大,可以将指向的对象和引用计数保存在同一个连续块中。有效地分配一个看起来像这样的对象(说明性,而不是字面意思)。

struct T {
    int reference_count;
    Object object;
};

因为引用计数和对象的生命周期是联系在一起的(一个比另一个活得更长没有意义)。这整个区块也可以同时是deleted。

所以效率在于分配,而不是复制(我怀疑这与优化有关)。

需要明确的是,这就是 boost 对 make_shared 所说的话

http://www.boost.org/doc/libs/1_43_0/libs/smart_ptr/make_shared.html

除了方便和风格之外,这样的功能也是异常安全的 并且相当快,因为​​它可以使用单个分配 对象及其相应的控制块,消除了 shared_ptr 的构造开销的很大一部分。这 消除了有关 shared_ptr 的主要效率投诉之一。

【讨论】:

  • 一个小问题,Object 实例可以在 refc 控制块之前死掉,即如果使用了weak_ptrs。这不是问题,除了对象布局直接持有的内存在控制块也死掉之前不会回收的小问题;在普通的 shared_ptr 中,对象的堆块可以在过期后立即回收。
【解决方案4】:

您不应该在那里获得任何额外的副本。输出应该是:

Create smart_ptr using make_shared...
Constructor make_shared
Create smart_ptr using make_shared: done.
Create smart_ptr using new...
Constructor new
Create smart_ptr using new: done.
Destructor

我不知道您为什么要获得额外的副本。 (虽然我看到你得到的“析构函数”太多了,所以你用来获取输出的代码必须与你发布的代码不同)

make_shared 更高效,因为它可以只使用一个动态分配而不是两个来实现,并且因为它需要一个指针的内存而不是每个共享对象的簿记。

编辑:我没有使用 Xcode 4.2 检查,但使用 Xcode 4.3 我得到了上面显示的正确输出,而不是问题中显示的错误输出。

【讨论】:

  • 谈好时机! ;-) 感谢 Xcode 4.3 报告。
猜你喜欢
  • 2021-08-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-07
  • 1970-01-01
  • 1970-01-01
  • 2020-09-12
相关资源
最近更新 更多