我正在阅读以下论文:http://www-db.in.tum.de/~leis/papers/ART.pdf并在其中,他们在摘要中说:
主存储器容量已经增长到大多数数据库的程度 适合RAM。对于主内存数据库系统,索引结构 性能是一个关键的瓶颈。传统的内存数据 像平衡二叉搜索树这样的结构效率不高 现代硬件,因为它们没有最佳地利用CPU缓存。 哈希表也经常用于主内存索引,但速度很快 仅支持点查询。
如何更好地理解CPU缓存的利用率以及它如何影响特定数据结构/算法的性能?
刚开始的某个地方会很棒,因为这种分析对我来说真的不透明,而且我不知道去哪里开始理解。
答案 0 :(得分:1)
这将是一个非常基本的答案,否则会非常广泛。我也不是这方面的专家(拿起点点滴滴来帮助理解如何更好地优化我的热点)。但它可能会帮助您开始调查此主题。
该主题让我想起了我大学时代的计算机架构 课程只讲授寄存器,DRAM和磁盘,同时还有光泽 在它之间的CPU缓存。 CPU缓存是其中之一 这些天的表现主导因素。
计算机的内存分为层次结构,范围从绝对最大但最慢(磁盘)到绝对最小但最快(寄存器)。
磁盘下面是DRAM,它仍然很慢。上面的寄存器是CPU缓存,速度非常快(特别是最小的L1缓存)。
访问一个节点
现在让我们假设您要求以某种形式从某些数据结构访问内存,比如树或链表等链接结构,我们只是访问一个节点。
注意,为了简单起见,我反转了内存访问的视图。通常情况下,它首先会有一条指令,要求将某些内容加载到一个注册表中,然后该过程会向后和向前工作,而不仅仅是前进。
虚拟到物理(DRAM)
在这种情况下,除非内存已经映射到物理内存,否则操作系统必须将页面从虚拟内存映射到DRAM中的物理地址(这种情况很慢,特别是在页面最糟糕的情况下故障涉及磁盘访问)。这通常是在相当大的块(机器抓住少量内存)中完成的,就像对齐的4千字节块一样。所以我们最终只为这个节点抓住一个4千字节大的内存大块。
DRAM到CPU缓存
既然这个4千字节的页面是物理映射的,我们仍然希望对节点做一些事情(大多数指令必须在寄存器级别运行),因此计算机将其向下移动通过CPU缓存层次结构(这很慢) )。通常,所有级别的CPU缓存都具有相同的缓存行大小,如英特尔上的64字节缓存行。
要将内存从DRAM移动到这些CPU缓存中,我们必须从DRAM中获取一大块缓存行大小和对齐的内存,并将其移动到CPU缓存中。我们可能还必须在途中将各种级别的CPU缓存层次结构中的某些数据逐出,例如最近最少使用的内存。所以现在我们为这个节点抓取一个64字节对齐的少量内存。
也许在这一点上,缓存行内存可能看起来像这样。我们假设相关的节点数据是42
,而???
中的内容是围绕它的无关内存,它不属于我们的链接数据结构。
要注册的CPU缓存
现在我们将内存从CPU缓存移动到寄存器(这种情况发生得非常快)。在这里,我们仍然会抓住一些极少的记忆,但却是一个非常小的记忆。例如,我们可能会获取一个64位对齐的内存块并将其移动到通用寄存器中。所以我们抓住了#34; 42"在这里并将其移动到注册表中。
最后,我们对寄存器执行一些操作并存储结果,结果通常会以内存层次结构的方式工作。
访问其他节点
当我们访问链接结构中的下一个节点时,我们最终必须重新执行此操作,只是为了读取一个小节点的数据。缓存行的内容可能如下所示(22
是感兴趣的节点数据)。
我们可以看到硬件和操作系统正在应用多少浪费,将大而对齐的数据块从较慢的内存移动到更快的内存,以便在驱逐之前访问它的一小部分内容。
这就是为什么所有小对象都被单独分配的原因,如链接节点或语言不能连续表示用户定义类型的情况,不是非常缓存或页面友好的。当我们遍历它们并访问它们的数据时,它们往往会调用很多页面错误和缓存未命中。也就是说,除非他们得到内存分配器的帮助,它以更连续的方式分配这些节点(在这种情况下,数据或两个或多个节点可能彼此相邻并一起访问)。
邻接度和空间位置
大多数缓存友好的数据结构倾向于基于连续的数组(它不必是一个巨大的数组,但可能是连接在一起的数组,例如,展开列表的情况)。当我们遍历一个数组并访问第一个元素时,我们可能不得不进行上述动作,但是一旦将内存移动到缓存行中,我们就可以得到这个:
现在我们可以遍历数组并访问所有元素,而它是机器上第二快的内存形式,即L1缓存,只是在初始强制缓存后将数据从L1缓存移动到寄存器错过/页面错误。如果我们从17
开始,我们有初始强制缓存未命中,但是可以访问此缓存行中的所有后续元素,而无需重复上述动作。这非常快,计算机可以查看这些数据。
这就是这部分的含义:
传统的内存数据结构,如平衡二进制搜索 树在现代硬件上效率不高,因为它们没有 最佳地利用CPU上的缓存。
请注意,可以使树和链表等链接结构比使用自定义内存分配器时更加缓存友好,但它们在基本数据结构级别缺乏这种固有的缓存友好性。
另一方面,散列表往往是基于数组的连续表结构。他们可能会使用链接和链接的存储桶结构,但是通过自定义分配器提供一点点帮助,这些也更容易实现缓存效率(远远低于树,因为哈希桶中的更简单的顺序访问模式)。所以无论如何,这是一个关于这个主题的简短概述,有点过于简单,但希望足以帮助你开始。如果您想更深入地理解这个主题,关键字将是缓存/内存效率/优化和参考位置。