繁体   English   中英

C ++ weak_ptr创建性能

[英]C++ weak_ptr creation performance

我已经读过创建或复制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复制到另一个慢?

这是我与游戏引擎的日子

故事如下:

我们需要一个快速的共享指针实现,不会破坏缓存(缓存现在更聪明btw)

正常指针:

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 => inline而且任何编译器都会内联一个运算符返回一个数据成员)它与普通指针的访问权限相同。

创建指针需要比正常情况多3个CPU指令(访问位置-4正在运行,添加1和写入位置-4)

现在我们在调试时只使用弱指针(因此我们使用DEBUG定义(宏定义)进行编译)因为那时我们希望看到所有的分配和最新情况等等。 这很有用。

弱指针必须知道他们指向的东西何时消失,不要保持他们指向的东西(在我的情况下,如果弱指针保持分配活着,引擎永远不会回收或释放任何记忆,那么它基本上是无论如何共享指针)

所以每个弱指针都有一个bool, 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”中。

当引用计数达到零时,您将浏览该列表(这是指向实际weak_pointers的指针列表,当它们显然被删除时它们将自行删除)并且您将alive = false(或其他内容)设置为每个weak_pointer。

weak_pointers现在知道他们指向的东西不再存在(所以在被引用时扔掉了)

在这个例子中

没有开销(系统的对齐是4个字节.64位系统往往喜欢8个字节的对齐....在那里将ref-counter与int [2]结合在一起以填充它。记住这个涉及到新闻(没有人投票,因为我提到它们:P)等等。你需要确保你对分配施加的struct与你分配和制作的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 stdlib实现很可爱。 你可以很容易地阅读它,它可以正常工作(遵循OO原则)但速度稍慢,并且可能会在最近蹩脚的芯片上破坏缓存。

UBER高性能笔记

您可能在想,为什么不拥有XXXX...XXXXiiii (最后的引用计数)然后您将得到最好的分配器对齐!

回答:

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

除了Alec对他之前项目中使用的shared / weak_ptr系统非常有趣的描述之外,我还想详细介绍一下典型的std::shared_ptr/weak_ptr实现可能发生的事情:

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

上述结构的主要费用是分配一块内存来保存两个引用计数。 这里不需要进行原子操作(除了在operator new下执行可能会或可能不会执行的operator new )。

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

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

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

这个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<shared_ptr<T>> )移动赋值之前的lhs == nullptr,因此根本没有原子操作。

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

当在容器中搜索智能指针时,例如在vector<shared_ptr<T>>::insert/erase ,将使用这些移动成员,并且与使用智能指针复制成员相比,可以产生可测量的积极影响。

我指出它,以便你知道如果你有机会移动而不是复制一个shared_ptr/weak_ptr ,那么输入一些额外的字符是值得的。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM