这是关于游戏引擎的经历:
需要一个快速的共享指针实现,不会使高速缓存失效(顺便说一下,现在的高速缓存更加智能了)。
一个普通指针:
XXXXXXXXXXXX....
^--pointer to data
我们的共享指针:
iiiiXXXXXXXXXXXXXXXXX...
^ ^
|
+
((unsigned int*)ptr)-1
指向计数的unsigned int*
。
这样,“共享指针”大小为指针大小,它包含的数据是实际数据的指针。因此(因为template=>
inline并且任何编译器都会内联返回数据成员的运算符),访问开销与普通指针相同。
创建指针比正常情况多花费了约3个CPU指令(访问位置-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的指针并使用异或技巧...好时光。
无论如何:指向shared_pointers所指向的弱引用放在一个列表中,在上面的"v"中以某种方式存储。
当引用计数变为零时,您会遍历该列表(该列表是指向实际weak_pointers的指针列表,它们在删除时会自动移除),并将alive=false(或其他)设置为每个weak_pointer。
现在,weak_pointers知道它们指向的内容已不存在(因此在取消引用时会抛出异常)
在这个例子中
没有额外开销(系统对齐为4字节。64位系统倾向于喜欢8字节对齐...在这种情况下,将ref-counter与int[2]联合起来进行填充。请记住,这涉及到原地new(别因为我提到它们而投票反对:P)等操作。您需要确保施加在分配上的struct
与您分配和创建的内容相匹配。编译器可以自行对齐(因此是int[2]而不是int, int)。
你可以完全无开销地取消引用shared_pointer。
制作新的shared pointer不会在缓存中产生任何影响,它们只需要3个CPU指令,虽然不太适合流水线处理,但编译器将始终内联getter和setter(如果不是总是:P),并且在调用站点周围会有一些东西来填充流水线。
shared pointer的析构函数也很简单(只有减量),因此非常好!
高性能注释
如果您遇到以下情况:
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原则正确地执行其工作,但速度略慢,并且可能会在这些日子里搞砸垃圾芯片的缓存。
超高性能说明
您可能会想,为什么不使用XXXX ... XXXXiiii
(最后的引用计数)然后您将获得最适合分配器的对齐方式!
答案:
因为执行
pointer+sizeof(T)
可能不止一条CPU指令!(减去4或8是CPU易于执行的操作,因为这样做很有意义,CPU会经常这样做)