我已经读过创建或复制std :: shared_ptr涉及一些开销(参考计数器的原子增量等).
但是如何从它创建一个std :: weak_ptr:
Obj * obj = new Obj(); // fast Obj * o = obj; // slow std::shared_ptra(o); // slow std::shared_ptr b(a); // slow ? std::weak_ptr c(b);
我希望在一些更快的性能,但我知道共享指针仍然必须递增弱引用计数器..所以这仍然像将shared_ptr复制到另一个慢?
除了Alec对他之前项目中使用的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);
此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>>
移动分配之前的大部分时间(例如在a内)lhs == nullptr,因此根本没有原子操作.
后者(weak_ptr
移动成员)实际上不是C++ 11,而是由LWG 2315处理.但是我希望它已经被大多数实现实现(我知道它已经在libc ++中实现).
当vector<shared_ptr<T>>::insert/erase
使用智能指针复制成员时,这些移动成员将用于在容器中搜索智能指针,例如在容器周围,并且可以具有可测量的积极影响.
我指出它,以便你知道如果你有机会移动而不是复制a shared_ptr/weak_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
这样一个"共享指针"是指针大小,它包含的数据是指向实际数据的指针.所以(因为模板=>
内联和任何编译器都会内联一个运算符返回一个数据成员)它与普通指针的访问是相同的"开销".
创建指针需要比正常情况多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
你对分配匹配你所分配和制作的东西.编译器可以自己对齐东西(因此int [2]不是int,int ).
您可以完全取消引用shared_pointer,而不需要任何开销.
正在制作的新共享指针根本不会破坏缓存并且需要3个CPU指令,它们不是很容易管道但是编译器总是会内联getter和setter(如果不是总是:P)那么'将成为可以填充管道的呼叫站点周围的东西.
共享指针的析构函数也很少(递减,就是这样),所以很棒!
高性能笔记
如果你有这样的情况:
f() { shared_pointer<T> ptr; g(ptr); }
无法保证优化器不敢通过将"按值"传递给shared_pointer来进行加法和减法.
这是您使用普通引用(实现为指针)的地方
所以你要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可以轻松完成的事情,因为它有意义,它会做很多事情)