std :: shared_ptr是否具有非原子等效项?为什么<memory>中没有一个?


88

这是一个由两部分组成的问题,全部有关std::shared_ptr

1. 据我所知,std::shared_ptr是唯一的智能指针在<memory>这原子。我想知道是否有std::shared_ptr可用的非原子版本(我在中看不到任何内容<memory>,所以我也愿意接受标准之外的建议,例如Boost中的建议)。我知道boost::shared_ptr也是原子的(如果BOOST_SP_DISABLE_THREADS未定义),但是也许还有另一种选择?我正在寻找具有与相同语义std::shared_ptr但没有原子性的东西。

2.我理解为什么std::shared_ptr是原子的;很好。但是,这并不是在每种情况下都很好,并且C ++历来有“只为所用内容付费”的口号。如果我不使用多个线程,或者如果我使用多个线程但不跨线程共享指针所有权,那么原子智能指针就显得过分了。我的第二个问题是,为什么std::shared_ptrC ++ 11中没有提供非原子版本的?(假设有一个原因)(如果答案仅仅是“根本就没有考虑过非原子版本”或“没有人问过非原子版本”就可以了!)。

对于问题2,我想知道是否有人提出了非原子版本的shared_ptr(向Boost或标准委员会提出)(不是取代原子版本的shared_ptr,而是与之共存)而被否决了。具体原因。


4
您到底在这里关心什么“成本”?原子递增整数的代价?对于任何实际应用,这实际上是否涉及您的成本?还是只是过早地进行优化?
Nicol Bolas

9
@NicolBolas:好奇心比什么都重要。我(目前)没有任何我要认真使用非原子共享指针的代码/项目。但是,在过去的项目中,Boost的shared_ptr原子性使其速度显着下降,并且定义BOOST_DISABLE_THREADS有明显的不同(我不知道std::shared_ptr它的成本是否与boost::shared_ptr以前相同)。
Cornstalks

13
@Close选民:问题的哪一部分没有建设性?如果没有第二个问题的具体原因,那很好(一个简单的“根本没有考虑”将是一个足够有效的答案)。我很好奇是否存在特定原因/理由。我会说,第一个问题当然是有效的问题。如果我需要澄清问题或对其进行一些调整,请告诉我。但是我看不出它没有什么建设性。
Cornstalks

10
@Cornstalks好吧,我想人们可能对人们可以轻易忽略为“过早优化”的问题的反应不佳,无论问题的有效性,合理性或相关性如何。对于我自己,我认为没有任何理由关闭它是非建设性的。
Christian Rau

13
(现在无法写出答案,因为它已关闭,因此请注释)。当您的程序不使用多个线程时,使用GCCshared_ptr不会将原子操作用于引用计数。请参阅gcc.gnu.org/ml/libstdc++/2007-10/msg00180.html上的(2),以获取针对GCC的补丁,以允许即使在多线程应用中也可以使用非原子实现,以实现shared_ptr对象之间不共享的对象线程。我已经坐在那个补丁上多年了,但我正在考虑最终将其提交给GCC 4.9
Jonathan Wakely

Answers:


104

1.我想知道是否有非原子版本的std :: shared_ptr

该标准未提供。“第三方”库可能提供了一个库。实际上,在C ++ 11之前和Boost之前,似乎每个人(包括我自己)都编写了自己的引用计数智能指针。

2.我的第二个问题是为什么C ++ 11中没有提供非原子版本的std :: shared_ptr?

这个问题在2010年的拉珀斯维尔会议上进行了讨论。该主题由瑞士的国家机构第20号评论介绍。辩论的双方都有很强的论点,包括您在问题中提供的论点。但是,在讨论结束时,投票以压倒性(但不是一致)反对添加非同步(非原子)版本的。shared_ptr

反对的观点包括:

  • 用不同步的shared_ptr编写的代码可能最终会在以后的线程代码中使用,最终导致在没有警告的情况下难以调试问题。

  • 拥有一个“通用” shared_ptr是引用计数中流量的“一种方式”,它的好处是:从原始建议中可以得出

    无论使用什么功能,都具有相同的对象类型,从而极大地促进了库(包括第三方库)之间的互操作性。

  • 原子的成本虽然不为零,但也不算太大。通过使用移动构造和移动分配(不需要使用原子操作)可以降低成本。这种操作通常用于vector<shared_ptr<T>>擦除和插入。

  • 如果这确实是他们想要做的,那么什么也不能阻止人们编写自己的非原子引用计数智能指针。

当天在拉珀斯维尔的LWG的最后一句话是:

拒绝CH20。目前尚无达成更改的共识。


7
哇,完美,感谢您提供的信息!这正是我希望找到的信息。
Cornstalks

>Has the same object type regardless of features used, greatly facilitating interoperability between libraries, including third-party libraries. 这是一个非常奇怪的推理。第三方库仍然会提供自己的类型,所以如果它们以std :: shared_ptr <CustomType>,std :: non_atomic_shared_ptr <CustomType>等形式提供它,为什么会很重要?你将永远有你的代码适应什么库返回反正
吉恩-迈克尔Celerier

就特定于库的类型而言,这是正确的,但想法是,在很多地方,第三方API中都会出现标准类型。例如,我的图书馆可能在std::shared_ptr<std::string>某个地方。如果其他人的库也采用这种类型,则调用者可以将相同的字符串传递给我们双方,而不会在不同表示形式之间进行转换带来麻烦或开销,这对每个人来说都是一个小小的胜利。
杰克·奥康纳

52

霍华德(Howard)已经很好地回答了这个问题,尼科尔(Nicol)就使用单一标准共享指针类型(而不是许多不兼容的指针类型)的好处提出了一些好的意见。

尽管我完全同意委员会的决定,但我确实认为在特殊情况下使用非同步shared_ptr类型会有所益处,因此我已经对该主题进行了几次调查。

如果我不使用多个线程,或者如果我使用多个线程但不跨线程共享指针所有权,那么原子智能指针就显得过分了。

使用GCC时,如果程序不使用多个线程,则shared_ptr不会将原子操作用于引用计数。这是通过包装器功能更新引用计数来完成的,该包装器功能可检测程序是否为多线程(在GNU / Linux上,只需检测程序是否链接至即可完成libpthread.so),并相应地分派给原子或非原子操作。

我早在多年前就意识到,由于GCCshared_ptr<T>是根据__shared_ptr<T, _LockPolicy>基类实现的,因此即使显式使用,即使在多线程代码中,也可以将基类与单线程锁定策略一起使用__shared_ptr<T, __gnu_cxx::_S_single>。不幸的是,由于这不是预期的用例,因此在GCC 4.9之前并不能很好地发挥作用,并且即使您明确请求了该_S_single策略,某些操作仍然使用包装函数,因此仍分派给原子操作。请参阅第(2)点,网址http://gcc.gnu.org/ml/libstdc++/2007-10/msg00180.html以获得更多详细信息和GCC补丁,以允许甚至在多线程应用中也使用非原子实现。我坐在那个补丁上已经有好几年了,但是我最终在GCC 4.9上提交了它,它允许您使用这样的别名模板来定义不是线程安全的共享指针类型,但是速度稍快一些:

template<typename T>
  using shared_ptr_unsynchronized = std::__shared_ptr<T, __gnu_cxx::_S_single>;

此类型不能与之互操作,std::shared_ptr<T>并且只有在保证shared_ptr_unsynchronized没有其他用户提供的同步就永远不会在线程之间共享对象时,才可以安全使用该类型。

当然,这是完全不可携带的,但是有时候可以。如果shared_ptr_unsynchronized<T>是的别名shared_ptr<T>,那么使用正确的预处理器技巧,您的代码将仍然可以与其他实现一起正常工作,对于GCC来说,它会更快一点。


如果您在4.9之前使用的是GCC,则可以通过在_Sp_counted_base<_S_single>您自己的代码中添加显式的特殊化(并确保没有人在__shared_ptr<T, _S_single>不包含特殊化的情况下进行实例化,以避免发生ODR冲突)来使用该std类型。可以在实践中工作,因为在这种情况下,我将专长添加到GCC还是将专长添加到自己的代码之间没有区别。


2
只是想知道,您的模板别名示例中是否有错字?即我认为应该读取shared_ptr_unsynchronized = std :: __ shared_ptr <。顺便说一句,我今天结合std :: __ enable_shared_from_this和std :: __ weak_ptr对此进行了测试,它似乎工作得很好(gcc 4.9和gcc 5.2)。我将对其进行概要分析/反汇编,以查看是否确实跳过了原子操作。
卡尔·库克

很棒的细节!最近我遇到一个问题,因为在描述这个问题,最终让我寻找到的源代码std::shared_ptrstd::__shared_ptr__default_lock_policy和这样的。这个答案证实了我对代码的理解。
纳瓦兹

21

我的第二个问题是为什么C ++ 11中没有提供非原子版本的std :: shared_ptr?(假设有一个原因)。

人们可以很容易地问,为什么没有侵入式指针,或者共享指针可能具有其他多种可能的变化。

shared_ptr由Boost继承的设计一直是创建最低标准的智能指针的通用语言。一般来说,您可以将其从墙上拉下来并使用。这是在各种应用程序中普遍使用的东西。您可以将其放在界面中,很可能好人会愿意使用它。

线程只会在未来变得越来越普遍。实际上,随着时间的流逝,线程化通常将是实现性能的主要手段之一。要求基本的智能指针完成支持线程所需的最低限度的工作,有助于实现这一目标。

将六个之间略有不同的智能指针倾倒到标准中,或者甚至更糟的是将基于策略的智能指针转储到标准中,这将是可怕的。每个人都会选择自己最喜欢的指针,并抛弃所有其他指针。没有人能够与任何人交流。就像C ++字符串的当前情况一样,每个人都有自己的类型。更糟糕的是,与字符串的互操作比在智能指针类之间的互操作要容易得多。

Boost和扩展委员会选择了要使用的特定智能指针。它提供了很好的功能平衡,并且在实践中被广泛使用。

std::vector在某些极端情况下,与裸露阵列相比,效率也较低。它有一些局限性。某些用途确实希望对a的大小进行硬性限制vector,而不使用抛出分配器。但是,该委员会并没有设计vector出适合所有人的一切。它被设计为大多数应用程序的理想默认值。那些无法使用的人可以编写满足其需求的替代方法。

就像您的智能指针一样,如果shared_ptr的原子性是一种负担。再说一遍,您可能还会考虑不要将它们复制太多。


7
+1表示“可能还会考虑不要将它们复制太多。”
阿里

如果您曾经连接过探查器,那么您很特别,您可以调出上述参数。如果您没有很难满足的操作要求,则不应使用C ++。像您这样争论是使C ++受到所有对高性能或低延迟感兴趣的人普遍谴责的好方法,这就是为什么游戏程序员不使用STL,Boost甚至异常的原因。
Hans Malherbe'3

为了清楚起见,我认为答案顶部的引号应为“为什么C ++ 11中没有提供非原子版本的std :: shared_ptr?”
Charles Savoie

4

我正在准备关于shared_ptr的工作。我一直在使用修改过的boost shared_ptr,避免使用单独的malloc(如make_shared可以做什么)和用于上述锁策略(如shared_ptr_unsynchronized)的模板参数。我正在使用来自的程序

http://flyingfrogblog.blogspot.hk/2011/01/boosts-sharedptr-up-to-10-slower-than.html

作为测试,在清理了不必要的shared_ptr副本之后。该程序仅使用主线程,并显示test参数。测试环境是运行linuxmint 14的笔记本。这是花费的时间(以秒为单位):

使用make_shared修改的boost进行测试运行安装boost(1.49)std
mt-unsafe(11)11.9 9 / 11.5(-pthread on)8.4  
原子(11)13.6 12.4 13.0  
mt-unsafe(12)113.5 85.8 / 108.9(-pthread on)81.5  
原子(12)126.0 109.1 123.6  

只有'std'版本使用-std = cxx11,并且-pthread可能会在g ++ __shared_ptr类中切换lock_policy。

从这些数字中,我看到了原子指令对代码优化的影响。测试用例不使用任何C ++容器,但是vector<shared_ptr<some_small_POD>>如果对象不需要线程保护,则可能会遭受损失。Boost遭受损失的可能性较小,因为附加的malloc限制了内联和代码优化的数量。

我还没有找到一台具有足够内核来对原子指令的可伸缩性进行压力测试的机器,但是仅在必要时使用std :: shared_ptr可能更好。


3

Boost提供shared_ptr的是非原子的。它称为local_shared_ptr,可以在boost的智能指针库中找到。


+1表示简短的可靠答复,并带有良好的引文,但是由于一种额外的间接级别(本地->共享-> ptr与共享-> ptr),这种指针类型看起来很昂贵-就内存和运行时间而言。
Red.Wave

@ Red.Wave您能解释一下间接意味着什么以及它如何影响性能吗?您的意思是shared_ptr即使它是本地的,它仍然带有计数器吗?还是您说这还有另一个问题?文档说唯一的区别是这不是原子的。
量子物理学家

每个本地ptr都会保留计数并引用原始共享的ptr。因此,对最终指点者的任何访问都需要从本地到共享ptr取消引用,然后再对指点者进行取消引用。因此,从共享的ptr到间接的堆栈最多还有一个间接的堆栈。这增加了开销。
Red.Wave19年

@ Red.Wave您从哪里获得此信息?这:“因此,对最终指点者的任何访问都需要从本地到共享ptr的取消引用”需要引用。我在boost文档中找不到。再次,我在文档中看到的是它说的local_shared_ptrshared_ptr除原子外都是相同的。我真的很想知道您的说法是否正确,因为我用local_shared_ptr在需要高性能的应用程序中。
量子物理学家

3
@ Red.Wave如果您查看实际的源代码github.com/boostorg/smart_ptr/blob / ...,您会发现没有双重间接。文档中的这一段只是一个心智模型。
伊利亚·波波夫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.