54

我正在使用C++11 进行试验,并编写了一个小玩具示例,以查看shared_ptr调用. 作为基础架构,我使用 llvm/clang 3.0 以及 XCode4 中的 llvm std c++ 库。make_sharedmake_shared

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:完成。

使用新创建 smart_ptr...

构造函数新

使用 new: done 创建 smart_ptr。

析构函数

析构函数

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

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

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

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

4

4 回答 4

41

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

嗯,这似乎是你的问题。C++11 标准在第 20.7.2.2.6 节中规定了make_shared<T>(and )的以下要求:allocate_shared<T>

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

T不需要是可复制构造的。事实上,T甚至不需要是非放置新的可构造的。它只需要可就地构建。这意味着唯一make_shared<T>可以做的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_ptrandmake_shared上,得到了和VC2010一样的东西。

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

于 2012-02-15T23:54:04.990 回答
33

您必须比较这两个版本:

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's 的角度来看,两个版本之间没有区别。更有效的是共享指针本身,而不是托管对象。)

于 2012-02-15T22:17:22.900 回答
6

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

也就是说,你用这个测试测量的并不是一种make_shared更有效的方法。简而言之,您测量的是错误的东西:-P。

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

因此,如果您正在创建一个对象,例如std::shared_ptr<Object> p2(new Object("foo"));There at least 2 calls to 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 的主要效率抱怨之一。

于 2012-02-15T22:25:11.960 回答
3

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

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 我得到了上面显示的正确输出,而不是问题中显示的错误输出。

于 2012-02-15T22:25:08.967 回答