【问题标题】:C++ weak_ptr creation performanceC++ weak_ptr 创建性能
【发布时间】:2013-11-29 17:06:01
【问题描述】:

我了解到创建或复制 std::shared_ptr 涉及一些开销(引用计数器的原子增量等)。

但是用它创建一个 std::weak_ptr 怎么样:

Obj * obj = new Obj();
// fast
Obj * o = obj;
// slow
std::shared_ptr<Obj> a(o);
// slow
std::shared_ptr<Obj> b(a);
// slow ?
std::weak_ptr<Obj> c(b);

我希望有更快的性能,但我知道共享指针仍然必须增加弱引用计数器。 那么这仍然和将 shared_ptr 复制到另一个一样慢吗?

【问题讨论】:

  • 做 1000000 次并分别测量 :)
  • @Drax 不鼓励这样做,现在的优化器(我很了解 GCC 的内脏——所以当然)实际上不会这样做很多次。模板的好处是编译器有定义(不仅仅是声明)。看我的回答。
  • 这里没有特定的语言版本;甚至特定于标准智能指针而不是提升指针。这是一个引用计数问题。

标签: c++ performance shared-ptr reference-counting weak-ptr


【解决方案1】:

这是我使用游戏引擎的日子

故事是这样的:

我们需要一个快速的共享指针实现,它不会破坏缓存(顺便说一句,缓存现在更智能了)

普通指针:

XXXXXXXXXXXX....
^--pointer to data

我们的共享指针:

iiiiXXXXXXXXXXXXXXXXX...
^   ^---pointer stored in shared pointer
|
+---the start of the allocation, the allocation is sizeof(unsigned int)+sizeof(T)

用于计数的unsigned int*位于((unsigned int*)ptr)-1

这样,“共享指针”是指针大小的,它包含的数据是指向实际数据的指针。所以(因为 template=&gt;inline 和任何编译器都会内联返回数据成员的运算符)它与普通指针的访问“开销”相同。

创建指针所需的 CPU 指令比正常多 3 条(访问位置 4 正在进行操作,添加 1 和写入位置 -4)

现在我们只在调试时使用弱指针(因此我们将使用定义的 DEBUG(宏定义)进行编译),因为那时我们希望查看所有分配以及正在发生的事情等等。这很有用。

弱指针必须知道他们指向的东西什么时候消失了,而不是让他们指向的东西保持活动状态(在我的例子中,如果弱指针保持分配活动,引擎将永远不会回收或释放任何内存,那么它基本上是一个共享指针)

所以每个弱指针都有一个布尔值,alive 什么的,并且是shared_pointer 的朋友

调试时我们的分配看起来像这样:

vvvvvvvviiiiXXXXXXXXXXXXX.....
^       ^   ^ the pointer we stored (to the data)
|       +that pointer -4 bytes = ref counter
+Initial allocation now 
    sizeof(linked_list<weak_pointer<T>*>)+sizeof(unsigned int)+sizeof(T)

您使用的链表结构取决于您关心的内容,我们希望尽可能接近 sizeof(T)(我们使用伙伴算法管理内存)所以我们存储了一个指向 weak_pointer 的指针并使用xor 技巧.... 好时光。

无论如何:指向 shared_pointers 指向的东西的弱指针被放在一个列表中,以某种方式存储在上面的“v”中。

当引用计数达到零时,您会遍历该列表(这是一个指向实际弱指针的指针列表,它们在明显被删除时会自行删除)并为每个弱指针设置 alive=false(或其他东西)。

weak_pointers 现在知道它们所指向的不再存在(在取消引用时会抛出)

在这个例子中

没有开销(系统的对齐是 4 个字节。64 位系统倾向于喜欢 8 个字节的对齐......在这种情况下,将 ref-counter 与 int[2] 联合起来以填充它.请记住,这涉及就地新闻(没有人反对,因为我提到了它们:P)等等。您需要确保您对分配施加的struct与您分配和制作的内容相匹配。编译器可以自己对齐内容(因此int [ 2] 不是 int,int)。

您可以毫无开销地取消引用 shared_pointer。

生成的新共享指针根本不会破坏缓存,并且需要 3 个 CPU 指令,它们不是非常...可流水线,但编译器将始终内联 getter 和 setter(如果不是总是:P)并且调用站点周围会有一些东西可以填充管道。

共享指针的析构函数也做的很少(减量,就是这样),所以很棒!

高性能说明

如果你有这样的情况:

f() {
   shared_pointer<T> ptr;
   g(ptr);
}

不保证优化器敢不做将shared_pointer“按值”传递给g的加减法。

这是你使用普通引用的地方(它被实现为指针)

所以你应该改用g(ptr.extract_reference()); - 编译器将再次内联简单的getter。

现在您有了一个 T&,因为 ptr 的作用域完全围绕 g(假设 g 没有副作用等等),该引用在 g 的持续时间内有效。

删除引用非常难看,您可能无法做到(我们依赖这个事实)。

事后看来

我应该创建一个名为“extracted_pointer”之类的类型,对于一个类成员来说,错误地输入它真的很难。

stdlib++ 使用的弱/共享指针

http://gcc.gnu.org/onlinedocs/libstdc++/manual/shared_ptr.html

没那么快……

但不要担心奇怪的缓存未命中,除非您正在制作一个运行不正常工作负载 > 120fps 的游戏引擎:P 仍然比 Java 好几英里。

stdlib 方式更好。每个对象都有自己的分配和工作。对于我们的shared_pointer,这是一个真实的案例,“相信我,它可以工作,尽量不要担心如何”(并不是说这很难),因为代码看起来真的很乱。

如果您撤消...无论他们在实现中对变量名称所做的一切,它都会更容易阅读。请参阅 Boost 的实现,如该文档中所述。

除了变量名之外,GCC 标准库的实现很可爱。您可以轻松阅读它,它可以正常工作(遵循 OO 原则),但速度稍慢,并且这些天可能会在糟糕的芯片上破坏缓存。

UBER 高性能说明

您可能在想,为什么不使用XXXX...XXXXiiii(最后的引用计数),那么您将获得分配器的最佳对齐方式!

答案:

因为必须执行pointer+sizeof(T) 可能不是一条CPU 指令! (减去 4 或 8 是 CPU 可以轻松完成的事情,因为这很有意义,它会经常这样做)

【讨论】:

  • 非常好的帖子,无论如何我一直在寻找我正在使用的 STL 版本。如果我需要速度,也许我会实现这样的东西!
  • @LukeGivens 你没看到那个链接吗?我也把它作为评论。事物的工作方式不是标准定义的,它必须具有的属性和名称是。
  • +1 顺便说一句,标准库编写者将双下划线放在变量名前以避免宏冲突。有a proposal to eliminate this
  • @TemplateRex 为什么不被接受!?你知道任何与 STL 有关的警告/错误有多可怕吗? (是的,你这样做,这是修辞!)。谢谢你告诉我,它解释了很多。
【解决方案2】:

除了Alec's 对他以前项目中使用的 shared/weak_ptr 系统的非常有趣的描述,我想更详细地说明典型的std::shared_ptr/weak_ptr 实现可能会发生什么:

// slow
std::shared_ptr<Obj> a(o);

上述构造的主要开销是分配一块内存来保存两个引用计数。这里不需要进行任何原子操作(除了在operator new 下实现可能会或可能不会做的事情)。

// slow
std::shared_ptr<Obj> b(a);

复制构造的主要开销通常是单个原子增量。

// slow ?
std::weak_ptr<Obj> c(b);

this weak_ptr 构造函数的主要开销通常是单个原子增量。我希望这个构造函数的性能与shared_ptr 复制构造函数的性能几乎相同。

另外两个需要注意的重要构造函数是:

std::shared_ptr<Obj> d(std::move(a));  // shared_ptr(shared_ptr&&);
std::weak_ptr<Obj> e(std::move( c ));  // weak_ptr(weak_ptr&&);

(以及匹配的移动赋值运算符)

移动构造函数根本不需要任何原子操作。他们只是将引用计数从 rhs 复制到 lhs,并使 rhs == nullptr。

仅当赋值之前的 lhs != nullptr 时,移动赋值运算符才需要原子减量。大部分时间(例如在 vector&lt;shared_ptr&lt;T&gt;&gt; 内)lhs == nullptr 在移动分配之前,因此根本没有原子操作。

后者(weak_ptr 移动成员)实际上不是 C++11,而是由 LWG 2315 处理。但是我希望它已经被大多数实现实现(我知道它已经在libc++ 中实现)。

这些移动成员将在容器中移动智能指针时使用,例如在vector&lt;shared_ptr&lt;T&gt;&gt;::insert/erase 下,与使用智能指针复制成员相比,可以产生可衡量的积极影响。

我指出这一点是为了让您知道,如果您有机会移动而不是复制 shared_ptr/weak_ptr,那么输入几个额外的字符是值得的。

【讨论】:

  • 再一次gcc.gnu.org/onlinedocs/libstdc++/manual/shared_ptr.html 你可以看到它比我概述的高性能的要重得多。这就是我概述它的原因。你会认为它会像我概述的那样工作,但不是。虽然它更通用且面向对象!
  • @AlecTeal:是的,我认为std::shared_ptr 是引用计数指针的“谢尔曼坦克”。它是防弹的。带有很多功能。并且不会像其他一些设计那样获得油耗。话虽如此,它已经经历了十多年的 boost 试验,并在 C++11 标准化方面经历了国际审查。委员会已经考虑并拒绝了其他一些方法,例如非线程安全的变体。您提供的 gcc 链接中记录的大多数功能都是实现细节,而不是标准指定。
猜你喜欢
  • 2011-02-14
  • 2013-10-09
  • 2012-06-04
  • 1970-01-01
  • 2013-10-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多