以数据导向设计的冒险 - 第3B部分:内部参考

2021-03-18 22:20:51

正如在最后一个博客帖子中所承诺的那样,今天我们将看一下分子如何处理发动机中其他系统所拥有的数据的内部引用。

首先,快速回顾我们不想使用用于引用数据的指针的原因是顺序:

使用原始指针,所有权有时候不清楚。如果我递给了一个指针,我是否需要删除实例?谁拥有它?我能坚持多久?这很快就会导致双删除和/或悬挂指针。两者都是各种各样的错误,如果你不幸,这很难找到。

以上,通过使用Shared_ptr<&gt可以稍微缓解。或者一些参考计数机制,但现在我们增加了额外的开销,这并非真正需要。所有权尚不清楚 - 参考指针背后的数据实际上是什么时候?还有谁坚持下去?

指针如何复制或复制,例如,跨网络?您总是必须有某种序列化机制,因为您不能仅在网络上发送指针 - 它们包含的地址在不同的地址空间中不会有意义。

指针不支持重定位。最终,拥有数据的系统也应负责管理数据的内存。因此,系统可能希望在内存中移动事物,例如,用于运行时碎片整理。通知每个可能持有指向系统内部数据的指针的每个实例都是繁琐和容易出错的。

所以,现在让我们仔细看看我们如何存储内部引用而不达到上述问题。

在分子发动机中,手柄用于指内部数据。也就是说,它们是指直接由某种系统拥有的数据,而不是通过某种间接。这也是他们称为内部参考的原因。

什么是处理?基本上,它们是数据的指数,但扭曲。人们可以将句柄视为“智能索引”。但在详细介绍句柄之前,普通索引已经解决了哪些问题?

您不能在索引上意外呼叫删除或免费()。此外,如果系统仅将索引涉及输入和输出参数,则应清楚该系统还拥有数据。

可以轻松复制和复制索引。它们还支持数据重定位出框外:如果我们希望在例如,例如访问数据。索引3,数据本身所在的位置并不重要,只要它保持相同的顺序。它可以驻留在地址0xa000或0xb000或某个elseplace - 数据[3]将给我们我们想要的数据。

我们无法检测到陈旧/删除数据的访问。我们可能会尝试访问索引3的数据,但自上次访问以来可能已经释放了。

整个数据块可以在内存中移动,但单个数据项的顺序无法更改,因为这会搞定我们的索引。

处理帮助我们有第一个问题,但也不支持任意重新安置各个数据项。这是ID或外部参考的是,但这些是下一篇文章的主题。

问题仍然存在:我们如何将指数转换为可以检测到已删除数据的访问的句柄?

这个想法非常简单:而不是仅使用索引,句柄还存储创建索引的生成。这一代只是一个单调增加的计数器,每次删除数据项时都会递增。该生成存储在手柄内,以及每个数据项。每当我们想要使用句柄访问数据时,索引的生成和数据项的生成需要匹配。

从上一篇文章返回我们的示例,让我们假设我们的渲染后端为4K顶点缓冲区提供空间。新的顶点缓冲区在内部使用池分配器/自由列表分配,用户只能处理VertexBufferHandle。

最初,我们的Vertex缓冲区池为空,并且所有世代都设置为零。

4096个顶点缓冲区: + ---- + ---- + ---- + ---- + ---- + ---- + | vb | vb | vb | .. | vb | vb | + ---- + ---- + ---- + ---- + ---- + ---- + 4096代: + ---- + ---- + ---- + ---- + ---- + ---- + | 0 | 0 | 0 | .. | 0 | 0 | + ---- + ---- + ---- + ---- + ---- + ---- +

首次分配顶点缓冲区,句柄将包含0的索引,而且将来的0生成0.将来的索引有一个不同的索引,也是0的生成。假设我们现在摧毁我们的第一个顶点缓冲区分配。包含它将增量的插槽的生成,产生以下布局:

+ ---- + ---- + ---- + ---- + ---- + ---- + | vb | vb | vb | .. | vb | vb | + ---- + ---- + ---- + ---- + ---- + ---- + + ---- + ---- + ---- + ---- + ---- + ---- + | 1 | 0 | 0 | .. | 0 | 0 | + ---- + ---- + ---- + ---- + ---- + ---- +

如果我们现在要使用句柄访问该顶点缓冲区,我们会检查其生成与带有我们的顶点缓冲区存储的那样,并发现它们不匹配 - 这意味着我们尝试访问已删除的数据。

我们尚未谈过的一件事是如何实现句柄。尽管如此,最简单的解决方案是最好的,因此在这种情况下,琐碎的结构就足够了:

在实践中,您通常不会为索引和生成使用两个32位整数,而是使用位域。在我们的顶点缓冲区句柄的情况下,我们需要12位用于在范围内存储索引[0,4095],如果我们希望我们的句柄是32位整数,则为生成离开20位。因此,我们的手柄看起来更像以下内容:

这意味着在1048576顶点缓冲区后的生成溢出已在我们池中的同一插槽中删除。从理论上讲,这意味着我们可以通过生成超过1048576个顶点缓冲区创建/删除周期前的旧句柄来错误地访问顶点缓冲区,在那个非常插槽中。在实践中,这不应该发生,除非我们为年龄存储旧句柄,否则会像疯狂一样创建/删除缓冲区,并不在使用该句柄使用该句柄访问缓冲区。然而,取决于您愿意花费的位数可能发生,因此需要记住。

最后但并非最不重要的是,关于我在上一个博客文章中提到的句柄的另一个好事是它们使用的内存不如指针。大多数手柄可以在单个32位整数中存储它们的索引和生成,这意味着它们需要与64位指针相比的一半内存量。此外,我们真的只需要将生成存储在句柄内,以检测对陈旧数据的访问。我们不应在零售版本中需要,因此,如果您的指标只需要在范围内,则句柄可以在那些构建中的16位整数中的一个小于16位整数。[0,65535]。

在分子中,我使用通用句柄实现,该句柄实现根据某些构建规则来定义底层数据类型,并且STITY_ASSERTS是位于该类型的位置。基本结构如下:

这结束了今天的帖子!在该系列的下一部分中,我们将讨论外部引用如何允许在内存中移动各个数据项,而无需关注的用户代码。