[英]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.