这是我使用游戏引擎的日子
故事是这样的:
我们需要一个快速的共享指针实现,它不会破坏缓存(顺便说一句,缓存现在更智能了)
一个普通的指针:
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(宏定义)进行编译),因为那时我们希望查看所有分配以及发生的事情等等。这很有用。
弱指针必须知道他们指向的东西什么时候消失了,而不是让他们指向的东西保持活动状态(在我的情况下,如果弱指针保持分配活动,引擎将永远不会回收或释放任何内存,那么它基本上就是无论如何都是共享指针)
所以每个弱指针都有一个 boolalive
或其他东西,并且是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 可以轻松完成的事情,因为这很有意义,它会经常这样做)