有没有办法针对永久对象的情况优化 shared_ptr?
Is there a way to optimize shared_ptr for the case of permanent objects?
我有一些代码在我的应用程序中广泛使用 shared_ptr
作为引用特定类型对象(我们称之为 T
)的标准方法。为了提高效率,我尽量小心地使用 make_shared
、std::move
和 const T&
。然而,我的代码花费了大量时间传递 shared_ptr
s(我在 shared_ptr
中包装的对象是整个 caboodle 的中心对象)。问题在于 shared_ptr
经常指向一个用作 "no value" 标记的对象;此对象是特定 T
子 class 的全局实例,并且它永远存在,因为它的引用计数永远不会变为零。
使用 "no value" 对象很好,因为它以很好的方式响应发送到这些对象的各种方法,以我希望 "no value" 的方式运行。但是,性能指标表明我的代码中有大量时间花在递增和递减该全局单例对象的引用计数上,创建引用它的新 shared_ptr
,然后销毁它们。即:对于一个简单的测试用例,如果我将 nullptr
卡在 shared_ptr
中以指示 "no value",而不是让它们指向全局单例 T
"no value" 对象。这是一个非常重要的区别。 运行 在更大的问题上,此代码将很快用于在计算集群上执行多天 运行s。所以我真的需要加速。但我真的很想也有我的 "no value" 对象,这样我就不必在我的代码中检查 nullptr
,特殊情况下这种可能性。
所以。有没有办法既吃我的蛋糕又吃它?特别是,我想象我可能会以某种方式 subclass shared_ptr
来制作一个 "shared_immortal_ptr" class 我可以与 "no value" 对象一起使用。 subclass 会像普通的 shared_ptr
一样工作,但它不会简单地增加或减少它的引用计数,并且会跳过所有相关的簿记。这样的事情可能吗?
我也在考虑制作一个内联函数,它将在我的 shared_ptr
上执行 get()
,如果 get()
返回 nullptr
;如果我在我的代码中到处使用它,并且从未在我的 shared_ptr
上直接使用 *
或 ->
,我想我会被隔离。
或者对于我没有想到的这种情况还有其他好的解决方案吗?
Galik 提出了关于您的收容策略的核心问题。我假设您已经考虑过这一点,并且有理由依赖 shared_ptr 作为一种没有其他选择的交流遏制策略。
我不得不提出一些看似有争议的建议。你定义的是你需要一种永远没有 nullptr 的 shared_ptr 类型,但 std::shared_ptr
不会那样做,我检查了各种版本的 STL 以确认客户删除器提供了不是解决方案的入口点。
因此,请考虑制作您自己的智能指针,或采用您根据需要更改的智能指针。基本思想是建立一种 shared_ptr ,它可以被指示将它的影子指针指向它不拥有的全局对象。
您有 std::shared_ptr
的来源。代码读起来不舒服。它可能很难使用。这是一种途径,但您当然会复制源代码、更改名称空间并实现您想要的行为。
然而,在 20 世纪 90 年代中期,当模板首次被引入那个时代的编译器时,我们所有人做的第一件事就是开始设计容器和智能指针。智能指针非常容易编写。它们更难设计(或曾经),但你有一个设计模型(你已经使用过)。
您可以实现 shared_ptr 的基本接口来创建替代品。如果您很好地使用了 typedef,那么您必须更改的地方应该有限,但至少搜索和替换会相当有效。
这是我建议的两种方式,两者都具有相同的功能。要么从库中采用 shared_ptr,要么从头开始制作。您会惊奇地发现更换的速度如此之快。
如果你采用std::shared_ptr
,主题将是了解shared_ptr如何确定它应该递减。在大多数实现中 shared_ptr 必须引用一个节点,在我的版本中它称为控制块 (_Ref)。当引用计数达到零时,该节点拥有要删除的对象,但如果 _Ref 为空,shared_ptr 自然会跳过该对象。但是,像 -> 和 * 这样的运算符,或者 get 函数,不会费心去检查 _Ref,它们只是 return 影子,或者在我的版本中是 _Ptr。
现在,调用重置时,_Ptr 将设置为 nullptr(或在我的源代码中为 0)。赋值给另一个对象或指针时会调用重置,因此即使使用对 nullptr 的赋值也能正常工作。关键是,对于您需要的这种新型 shared_ptr,您可以简单地更改行为,以便每当发生这种情况时(重置为 nullptr),您设置 _Ptr,即 shared_ptr 中的影子指针,到 "no value global" 对象的地址。
所有 *、get 或 -> 的使用都将 return 那个无值对象的 _Ptr,并且在另一个赋值中使用时会正确运行,或者再次调用 reset,因为这些函数不会依靠影子指针作用于节点,并且由于在这种特殊情况下节点(或控制块)将为 nullptr,shared_ptr 的其余部分将表现得好像它正确指向 nullptr - 也就是说,不删除全局对象。
显然,将 std::pointer
更改为此类特定于应用程序的行为听起来很疯狂,但坦率地说,这就是性能工作往往让我们做的事情;其他奇怪的事情,比如偶尔放弃 C++ 以获得 C 或汇编程序的更多原始速度。
修改 std::shared_ptr
源代码,作为特殊用途的副本,这不是我会选择的(而且,事实上,我遇到过你这种情况的其他版本,所以我做了几次这个选择几十年的倍数)。
为此,我建议您构建一个基于策略的智能指针。我觉得很奇怪,我早些时候在另一个 post 今天(或昨天,它是 1:40am)提出了这个建议。
我参考了 Alexandrescu 2001 年的书(我认为那是现代 C++...还有一些我不记得的词)。他展示了 loki,其中包括基于策略的智能指针设计,该设计仍在他的网站上发布并免费提供。
在我看来,这个想法应该被纳入 shared_ptr。
基于策略的设计是作为模板的范例实现的 class 从它的一个或多个参数派生而来,如下所示:
template< typename T, typename B >
class TopClass : public B {};
通过这种方式,您可以提供 B,从中构建对象。现在,B 可能具有相同的构造,它也可能是从它的第二个参数派生的策略级别(或多个派生,但是设计有效)。
层可以组合以实现各种类别的独特行为。
例如:
std::shared_ptr
和 std::weak_ptr
是独立的 classes,它们作为一个家族与其他(节点或控制块)交互以提供智能指针服务。然而,在我多次使用的设计中,这两个是由同一个顶级模板构建的class。该设计中 shared_ptr 和 weak_ptr 之间的区别在于模板的第二个参数中提供的附件策略。如果类型是使用弱附加策略作为第二个参数实例化的,那么它就是一个弱指针。如果它被赋予强附件策略,它就是一个智能指针。
创建策略设计模板后,您可以引入原始设计中没有的层(扩展它),或 "intercept" 行为并像您当前需要的那样对其进行专门化 - 而不会破坏原始代码或设计。
我开发的智能指针库具有很高的性能要求,以及许多其他选项,包括自定义内存分配和自动锁定服务,以使写入智能指针线程安全(std::shared_ptr
不提供).接口和大部分代码是共享的,但可以通过选择不同的策略简单地形成几种不同类型的智能指针。要更改行为,可以在不更改现有代码的情况下插入新策略。目前,我同时使用std::shared_ptr(我几年前在 boost 时使用过)和我几年前开发的 MetaPtr 库,当我需要高性能或灵活的选项时使用后者,就像你的一样。
如果 std::shared_ptr 是一个基于策略的设计,正如 loki 所展示的那样,您可以使用 shared_ptr 执行此操作,而无需复制源并将其移动到新的命名空间。
在任何情况下,只需创建一个共享指针,将影子指针指向重置为 nullptr 的全局对象,让节点指向 null,即可提供您描述的行为。
我有一些代码在我的应用程序中广泛使用 shared_ptr
作为引用特定类型对象(我们称之为 T
)的标准方法。为了提高效率,我尽量小心地使用 make_shared
、std::move
和 const T&
。然而,我的代码花费了大量时间传递 shared_ptr
s(我在 shared_ptr
中包装的对象是整个 caboodle 的中心对象)。问题在于 shared_ptr
经常指向一个用作 "no value" 标记的对象;此对象是特定 T
子 class 的全局实例,并且它永远存在,因为它的引用计数永远不会变为零。
使用 "no value" 对象很好,因为它以很好的方式响应发送到这些对象的各种方法,以我希望 "no value" 的方式运行。但是,性能指标表明我的代码中有大量时间花在递增和递减该全局单例对象的引用计数上,创建引用它的新 shared_ptr
,然后销毁它们。即:对于一个简单的测试用例,如果我将 nullptr
卡在 shared_ptr
中以指示 "no value",而不是让它们指向全局单例 T
"no value" 对象。这是一个非常重要的区别。 运行 在更大的问题上,此代码将很快用于在计算集群上执行多天 运行s。所以我真的需要加速。但我真的很想也有我的 "no value" 对象,这样我就不必在我的代码中检查 nullptr
,特殊情况下这种可能性。
所以。有没有办法既吃我的蛋糕又吃它?特别是,我想象我可能会以某种方式 subclass shared_ptr
来制作一个 "shared_immortal_ptr" class 我可以与 "no value" 对象一起使用。 subclass 会像普通的 shared_ptr
一样工作,但它不会简单地增加或减少它的引用计数,并且会跳过所有相关的簿记。这样的事情可能吗?
我也在考虑制作一个内联函数,它将在我的 shared_ptr
上执行 get()
,如果 get()
返回 nullptr
;如果我在我的代码中到处使用它,并且从未在我的 shared_ptr
上直接使用 *
或 ->
,我想我会被隔离。
或者对于我没有想到的这种情况还有其他好的解决方案吗?
Galik 提出了关于您的收容策略的核心问题。我假设您已经考虑过这一点,并且有理由依赖 shared_ptr 作为一种没有其他选择的交流遏制策略。
我不得不提出一些看似有争议的建议。你定义的是你需要一种永远没有 nullptr 的 shared_ptr 类型,但 std::shared_ptr
不会那样做,我检查了各种版本的 STL 以确认客户删除器提供了不是解决方案的入口点。
因此,请考虑制作您自己的智能指针,或采用您根据需要更改的智能指针。基本思想是建立一种 shared_ptr ,它可以被指示将它的影子指针指向它不拥有的全局对象。
您有 std::shared_ptr
的来源。代码读起来不舒服。它可能很难使用。这是一种途径,但您当然会复制源代码、更改名称空间并实现您想要的行为。
然而,在 20 世纪 90 年代中期,当模板首次被引入那个时代的编译器时,我们所有人做的第一件事就是开始设计容器和智能指针。智能指针非常容易编写。它们更难设计(或曾经),但你有一个设计模型(你已经使用过)。
您可以实现 shared_ptr 的基本接口来创建替代品。如果您很好地使用了 typedef,那么您必须更改的地方应该有限,但至少搜索和替换会相当有效。
这是我建议的两种方式,两者都具有相同的功能。要么从库中采用 shared_ptr,要么从头开始制作。您会惊奇地发现更换的速度如此之快。
如果你采用std::shared_ptr
,主题将是了解shared_ptr如何确定它应该递减。在大多数实现中 shared_ptr 必须引用一个节点,在我的版本中它称为控制块 (_Ref)。当引用计数达到零时,该节点拥有要删除的对象,但如果 _Ref 为空,shared_ptr 自然会跳过该对象。但是,像 -> 和 * 这样的运算符,或者 get 函数,不会费心去检查 _Ref,它们只是 return 影子,或者在我的版本中是 _Ptr。
现在,调用重置时,_Ptr 将设置为 nullptr(或在我的源代码中为 0)。赋值给另一个对象或指针时会调用重置,因此即使使用对 nullptr 的赋值也能正常工作。关键是,对于您需要的这种新型 shared_ptr,您可以简单地更改行为,以便每当发生这种情况时(重置为 nullptr),您设置 _Ptr,即 shared_ptr 中的影子指针,到 "no value global" 对象的地址。
所有 *、get 或 -> 的使用都将 return 那个无值对象的 _Ptr,并且在另一个赋值中使用时会正确运行,或者再次调用 reset,因为这些函数不会依靠影子指针作用于节点,并且由于在这种特殊情况下节点(或控制块)将为 nullptr,shared_ptr 的其余部分将表现得好像它正确指向 nullptr - 也就是说,不删除全局对象。
显然,将 std::pointer
更改为此类特定于应用程序的行为听起来很疯狂,但坦率地说,这就是性能工作往往让我们做的事情;其他奇怪的事情,比如偶尔放弃 C++ 以获得 C 或汇编程序的更多原始速度。
修改 std::shared_ptr
源代码,作为特殊用途的副本,这不是我会选择的(而且,事实上,我遇到过你这种情况的其他版本,所以我做了几次这个选择几十年的倍数)。
为此,我建议您构建一个基于策略的智能指针。我觉得很奇怪,我早些时候在另一个 post 今天(或昨天,它是 1:40am)提出了这个建议。
我参考了 Alexandrescu 2001 年的书(我认为那是现代 C++...还有一些我不记得的词)。他展示了 loki,其中包括基于策略的智能指针设计,该设计仍在他的网站上发布并免费提供。
在我看来,这个想法应该被纳入 shared_ptr。
基于策略的设计是作为模板的范例实现的 class 从它的一个或多个参数派生而来,如下所示:
template< typename T, typename B >
class TopClass : public B {};
通过这种方式,您可以提供 B,从中构建对象。现在,B 可能具有相同的构造,它也可能是从它的第二个参数派生的策略级别(或多个派生,但是设计有效)。
层可以组合以实现各种类别的独特行为。
例如:
std::shared_ptr
和 std::weak_ptr
是独立的 classes,它们作为一个家族与其他(节点或控制块)交互以提供智能指针服务。然而,在我多次使用的设计中,这两个是由同一个顶级模板构建的class。该设计中 shared_ptr 和 weak_ptr 之间的区别在于模板的第二个参数中提供的附件策略。如果类型是使用弱附加策略作为第二个参数实例化的,那么它就是一个弱指针。如果它被赋予强附件策略,它就是一个智能指针。
创建策略设计模板后,您可以引入原始设计中没有的层(扩展它),或 "intercept" 行为并像您当前需要的那样对其进行专门化 - 而不会破坏原始代码或设计。
我开发的智能指针库具有很高的性能要求,以及许多其他选项,包括自定义内存分配和自动锁定服务,以使写入智能指针线程安全(std::shared_ptr
不提供).接口和大部分代码是共享的,但可以通过选择不同的策略简单地形成几种不同类型的智能指针。要更改行为,可以在不更改现有代码的情况下插入新策略。目前,我同时使用std::shared_ptr(我几年前在 boost 时使用过)和我几年前开发的 MetaPtr 库,当我需要高性能或灵活的选项时使用后者,就像你的一样。
如果 std::shared_ptr 是一个基于策略的设计,正如 loki 所展示的那样,您可以使用 shared_ptr 执行此操作,而无需复制源并将其移动到新的命名空间。
在任何情况下,只需创建一个共享指针,将影子指针指向重置为 nullptr 的全局对象,让节点指向 null,即可提供您描述的行为。